CLI Safety
CLI Safety — the Non-Project Guard
Section titled “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
Section titled “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
Section titled “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*)
Section titled “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
Section titled “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.