| name | sandbox-preflight |
| description | At the start of a dispatched lifecycle (runner or bare dispatch), verify the tools you need are in the sandbox allowlist. Fail LOUD on a blocked tool -- do NOT produce a "run this yourself" artifact (the silent-degradation trap). For a known-safe set of common dev tools, auto-heal by adding to .claude/settings.local.json. Anything else: ask the user. |
| allowed-tools | Bash(git *) Bash(gh *) |
Sandbox preflight
Before doing any real work, verify that the tools you'll need are
actually runnable in the sandbox. The dangerous failure mode this
prevents is silent degradation: a blocked tool (gh, cargo,
etc.) makes the agent emit an output that looks complete -- a
"run this command yourself" markdown artifact, or a report full of
BLOCKED lines -- while no real state changed. The dispatcher
gets a "completed" signal and trusts the wrong state.
Two work-machine reports surfaced the same root cause:
- roba #112: the runner returned a "run this yourself" markdown
block when
gh wasn't in the allowlist. Output looked finished;
no GitHub state changed; the synchronous-lifecycle "complete"
contract silently broke.
- roba #113: a bare release-audit dispatch ran static checks but
couldn't run the build gate (
cargo fmt/clippy/test,
maturin build) because those tools weren't allowlisted. It asked
"want me to add the allowlist?" as a question and accumulated
BLOCKED entries instead of acting.
Both are sandbox-permission issues. The fix is one discipline:
check first, fail loud, auto-heal the known-safe set, ask for
anything else.
When to apply
- Runner step 0. Before fetching the issue, before composing any
prompt. Referenced from
../../agents/runner.md.
- Bare dispatches that will use build tools,
gh, git,
or any tool beyond Read/Glob/Grep. The
orchestration-prompt-template
skill should add a pre-flight section near the top of such a
prompt's steps.
If the work is genuinely read-only (a Q&A or explainer that only
needs Read/Glob/Grep), no pre-flight is needed.
Tools to verify
A minimum set the runner always needs:
gh -- GitHub CLI (issue / PR / CI-watch operations)
git -- branch + commit + push
Plus project-detected tools, keyed off files in the repo root:
| Detected file | Tools to verify |
|---|
Cargo.toml | cargo |
pyproject.toml / setup.py | pip, uv, maturin (whichever the project references) |
package.json | npm (and pnpm / yarn if its lockfile is present) |
go.mod | go |
mix.exs | mix |
pom.xml | mvn |
build.gradle / build.gradle.kts | gradle |
Makefile / build.sh | make / bash |
Check each by attempting a no-op version probe -- <tool> --version
(or a tool-appropriate equivalent). If the call is blocked by the
sandbox (not merely "command not found"), the tool is not in the
allowlist.
Step 0: the write-gate probe
Bash allowlisting is only half the surface. A file-mutating dispatch
launched in default permission mode also hits the Edit/Write approval
gate on its first write -- "haven't granted it yet" -- and the Bash probes
above will not catch it. This is a distinct gate from the Bash allowlist, and
it is the most common silent-failure mode for editing dispatches: the session
either stalls silently (fire-and-forget no-op, zero work done) or grinds
through mechanical bypasses (touch + git apply heredocs, tee, perl,
python redirects) for dozens of turns, producing corrupted output.
So before any real edit, probe the write gate directly -- exercise the
actual Edit/Write tool, not a Bash command:
- Pick a scratch path the task will touch. If the task creates a new
top-level directory (e.g.
skills/, src/new-module/), probe a scratch
file inside that new directory -- new-directory creation is a distinct
write-gate trigger from editing an existing file, and a common one. If the
task only edits existing files, probe a scratch path in the task's cwd.
- Attempt a trivial
Write to that scratch path (e.g. one line of content).
- Expected pass: the write succeeds. Delete the scratch file and proceed.
- Expected fail: the write returns the approval/permission gate message
("haven't granted", "permission to write", or similar). Treat this exactly
like a blocked Bash tool: fail loud immediately with the exact gate
message and do NOT proceed. The launch configuration is wrong -- the
dispatch needs
--full-auto or --permission-mode acceptEdits (or the
Task-tool equivalent). Workarounds cannot fix a misconfigured launch.
Discipline (carry this into the run, not just preflight): if a write is
blocked by a permission/approval gate -- not a logic error -- STOP immediately
and report the exact gate message. Do NOT attempt mechanical bypasses
(touch + git apply, perl redirect, tee, python script). The launch
configuration is wrong; workarounds cannot fix it.
The fail-loud rule
Never produce a "run this yourself" artifact. If a tool you need
is blocked AND it is not in the auto-heal allowlist below, ABORT the
lifecycle with a clear message and do NOT proceed:
ABORTED at sandbox preflight: <tool> is not in the sandbox allowlist.
To unblock, add the following to .claude/settings.local.json under
"permissions.allow":
"Bash(<tool>:*)"
Then re-dispatch this task.
The dispatcher sees an abort, not a fake "complete." That is
exactly what the synchronous-lifecycle contract requires for trust:
a blocked run is a legitimate hand-back, not a silently degraded
success.
Auto-heal allowlist
For these well-known dev tools, self-heal by adding the appropriate
Bash(<tool>:*) entry to .claude/settings.local.json instead of
aborting:
gh, git
cargo (when Cargo.toml is present)
npm, pnpm, yarn (when package.json is present)
pip, uv, maturin (when pyproject.toml / setup.py is
present)
go (when go.mod is present)
mix (when mix.exs is present)
mvn (when pom.xml is present)
gradle (when build.gradle or build.gradle.kts is present)
make, bash, sh (always -- they're the universal build glue)
Self-healing is a contained action: it writes a single allowlist
entry, surfaces what it added in the return summary, and proceeds.
The user sees the addition in the returned report -- nothing happens
silently.
Anything outside this list: ASK the user before adding. Tools
like docker, kubectl, terraform, or custom scripts are
security-sensitive enough to require explicit consent. Don't
auto-heal them; surface the need and wait.
What to surface
- Pre-flight passes cleanly: say nothing; proceed.
- Pre-flight self-heals: include in the return summary
Auto-healed: added Bash(<tool>:*) to .claude/settings.local.json.
- Pre-flight aborts: emit the ABORT message above; do NOT proceed
with the rest of the lifecycle.
Anti-patterns
- Producing "run this yourself" artifacts when a tool is blocked -- the dispatcher gets a fake "complete" signal.
- Silently degrading to a partial run when a needed tool is missing from the allowlist -- abort loudly instead.
- Auto-healing tools outside the known-safe list (docker, kubectl, terraform) -- these require explicit user consent.
- Probing only Bash allowlisting and skipping the Step 0 write-gate probe -- a
default-mode editing dispatch passes the Bash checks then stalls or
corrupts on the first blocked Edit/Write.
- Grinding through mechanical write bypasses (
touch + git apply, tee,
perl, python redirect) after a blocked write -- the launch config is
wrong; STOP and report the gate message instead.
Related