Skip to content

CLI Safety

Which BoB CLI commands are destructive when run outside a project, and how each one is guarded (#707). (Back to reference index.)

Adjacent concern: the intent-mutating vs pass-through taxonomy (#784) classifies which cd* invocations edit a project’s provision manifest — that governs reviewability (the stage-on-branch gate, #785), while this guard governs where the tools may run at all. See the handbook §3.4.

scripts/lib/project-guard.sh exposes assert_project_dir <dir> [--force], the shared safety floor for every command that mutates its target directory. It refuses to proceed when the target is:

Target Return code Overridable?
$HOME 2 No — hard-denied unconditionally
BOB_HOME (~/.claude) 3 No — it is a deploy target, not a project (#411)
A directory with no project marker 1 Yes — --force or BOB_ALLOW_NONPROJECT=1
A missing/unreadable directory 4 No — nothing to operate on

Project markers: .git (directory or worktree gitlink file), package.json, wrangler.toml, wrangler.jsonc, dev.json, CLAUDE.md — aligned with cdi’s stack auto-detection. A pre-existing .claude/ is deliberately not required, since bob init legitimately creates it.

$HOME and BOB_HOME are refused even when they carry markers (both typically contain CLAUDE.md): hard-deny beats markers. Treating BOB_HOME as a project is the exact deployed-copy inversion #411 exists to prevent, so the override does not unlock it.

Override. For intentional runs in a marker-less directory (e.g. an empty directory for a brand-new project), pass --force (where the command exposes it) or set BOB_ALLOW_NONPROJECT=1. The override path logs a warning to stderr and proceeds; it applies only to the marker check.

Fail-open with warning. A command that cannot find the guard library (e.g. an out-of-date BOB_HOME before the next deploy.sh sync) warns to stderr and continues, so a stale deployment never bricks the CLI. Run scripts/deploy.sh to pick up the guard.

Tests: tests/test-project-guard.sh (make test-project-guard).

What each command would do to a non-project directory, and what stops it:

Since #2060 each engine lives at scripts/cli/<name> and is reached as a bob noun; bin/<name> is the generated alias shim that execs the same route, so the guard is the engine’s wherever it is entered.

Command Entry point Destructive in a non-project dir? Guard
cdi scripts/cli/cdi — bob init; bin/cdi is its alias shim (deployed to ~/.claude/bin/cdi; root claude-init.sh remains as a one-release compat passthrough, #1193) Yes — creates .claude/ + INIT.lock, writes .gitignore, symlinks tooling, installs git hooks, allocates a port band, runs provisioning Sources project-guard.sh before any filesystem mutation; --force / BOB_ALLOW_NONPROJECT=1 override the marker check
bob provision scripts/cli/cdprov (bob provision) → scripts/orchestrator/provision.js Yes for mutating actions (init writes a manifest named after the directory, refresh symlinks registry items into .claude/, prune deletes symlinks, interview writes a manifest) Guard runs for init / refresh / prune / interview only; status / diff / check are read-only and stay usable anywhere; BOB_HOME routes to the read-only verify path (#411) before the guard is consulted
bob dev up scripts/cli/dev-up (bob dev up) Yes — find_project_root falls back to $PWD, then seeds a dev.json and starts servers there Sources project-guard.sh after root resolution, before any write; BOB_ALLOW_NONPROJECT=1 override
bob fork / bob pair scripts/cli/cdfork (bob fork / bob pair) → scripts/cdfork/ Yes in principle (worktrees, branches, tmux sessions) Existing equivalent: requires a git repository — worktree/branch operations fail fast outside one. Inherits the guard indirectly when its warp-drive sessions run bob dev up/bob provision
bob swarm scripts/cli/swarm (bob swarm) → scripts/swarm/run.js Composes bob fork dispatch Same existing equivalent as bob fork (git repo required)
bob warp scripts/cli/warp (bob warp <verb>) No — observes/controls warp-drive state; refuses when no project root is found find_project_root errors out (no $PWD fallback)
bob dev health scripts/cli/dev-health (bob dev health) No — read-only health probe n/a

bob dash status, bob dash generate, bob dash serve, bob promote, bob link live in the dotfiles repo (~/bin), not BOB_SOURCE — this inventory cannot wire them. Of these, bob dash generate (writes a dashboard), bob promote (promotes files into BOB_SOURCE), and bob link (creates symlinks) mutate their target and should source ~/.claude/scripts/lib/project-guard.sh the same way bob init does; bob dash status and bob dash serve are read-mostly. Tracking the dotfiles wiring is a follow-up outside this repo — the guard library is deployed and stable at ~/.claude/scripts/lib/project-guard.sh for them to source.

2026-06-27: bob init was accidentally run in $HOME. It was harmless only because BOB_SOURCE was unset and $HOME is not a git repository — with BOB_SOURCE set it would have inverted deployed copies into repo symlinks and mutated protected machine-specific files. The guard makes that class of accident structurally impossible rather than luck-dependent.