Skip to content

CLI Safety — the Non-Project Guard

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.

The guard

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:

TargetReturn codeOverridable?
$HOME2No — hard-denied unconditionally
BOB_HOME (~/.claude)3No — it is a deploy target, not a project (#411)
A directory with no project marker1Yes — --force or BOB_ALLOW_NONPROJECT=1
A missing/unreadable directory4No — 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).

Destructive-command inventory

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

CommandEntry pointDestructive in a non-project dir?Guard
cdibin/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 provisioningSources project-guard.sh before any filesystem mutation; --force / BOB_ALLOW_NONPROJECT=1 override the marker check
cdprovbin/cdprovscripts/provision.shYes 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-upbin/dev-upYes — find_project_root falls back to $PWD, then seeds a dev.json and starts servers thereSources project-guard.sh after root resolution, before any write; BOB_ALLOW_NONPROJECT=1 override
cdfork / cdfork-pairbin/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
swarmbin/swarmscripts/swarm/run.jsComposes cdfork dispatchSame existing equivalent as cdfork (git repo required)
cdprojbin/cdprojscripts/gh-projects/cdproj.jsMutates the linked GitHub Project (remote), not the local directoryExisting equivalent: requires gh repo context — fails without a git repo + remote
warpbin/warpNo — observes/controls warp-drive state; refuses when no project root is foundfind_project_root errors out (no $PWD fallback)
dev-healthbin/dev-healthNo — read-only health proben/a

Cross-repo follow-ups (dotfiles cd*)

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.

Incident context

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.