Appearance
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:
| 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).
Destructive-command inventory
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/cdprov → scripts/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/cdfork → 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 dev-up/cdprov |
swarm | bin/swarm → scripts/swarm/run.js | Composes cdfork dispatch | Same existing equivalent as cdfork (git repo required) |
cdproj | bin/cdproj → scripts/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 |
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.