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 cdi 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:

Command Entry point Destructive in a non-project dir? Guard
cdi bin/cdi (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
cdprov bin/cdprovscripts/provision.sh 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
dev-up bin/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
cdfork / cdfork-pair bin/cdforkscripts/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 dev-up/cdprov
swarm bin/swarmscripts/swarm/run.js Composes cdfork dispatch Same existing equivalent as cdfork (git repo required)
cdproj bin/cdprojscripts/gh-projects/cdproj.js Mutates the linked GitHub Project (remote), not the local directory Existing equivalent: requires gh repo context — fails without a git repo + remote
warp bin/warp No — observes/controls warp-drive state; refuses when no project root is found find_project_root errors out (no $PWD fallback)
dev-health bin/dev-health No — read-only health probe n/a

cdb, cdg, cds, cdp, cdl live in the dotfiles repo (~/bin), not BOB_SOURCE — this inventory cannot wire them. Of these, cdg (writes a dashboard), cdp (promotes files into BOB_SOURCE), and cdl (creates symlinks) mutate their target and should source ~/.claude/scripts/lib/project-guard.sh the same way cdi does; cdb and cds 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: cdi 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.