What an allow rule really grants
An allow rule does not make a call safe; it removes the question. Before writing one it is worth being precise about how much question is being removed, because the text of a rule can read far narrower than its reach.
A command is everything it runs
allow: ["bash(make)"] reads as a statement about make. It is a statement
about every recipe in the makefile, which is to say, about anything at all.
An allow rule for a build command is transitively an allow rule for whatever
that build executes, in every mode including ask. The same holds for a test
runner, a package manager's install step, anything that takes its instructions
from files in the workspace. Those files are exactly what a coding agent edits.
crucible has no built-in list of paths that always ask, and that is
deliberate: every build tool ships a hook file, so such a list can never be
complete, and an incomplete list that looks like protection stops being
thought about. If a hook or a build script deserves guarding in your project,
write the ask or deny rule about it: visible, and yours.
Wrapper programs cannot be allowed
For some programs the thing that runs is an argument: timeout 5 curl example.com is a curl call, and sudo cargo test is a cargo call with
the safety off. A rule matched against the program's name would say timeout
and mean anything at all, so a command containing one of these is treated as a
command whose text does not say what will run: no rule short of a blanket
covers it, and it is asked about every time.
This is structural rather than a shortlist of three familiar shells. It covers
launchers such as sudo, env, timeout, xargs, chroot and ssh; shell
builtins and grammar that reinterpret what follows; shell names and their
versioned forms; and interpreter families including Python, Node, PowerShell,
Perl, Ruby, Lua, awk (with gawk and mawk) and sed, and their versioned
executables. A quoted, escaped, expanded or
otherwise computed program name is refused too. Changing from sh to dash,
or from python to python3.14, cannot make the same narrow-looking rule begin
to match.
allow: ["bash(timeout *)"] therefore never fires. That is the point: a rule
whose author could not have known what they authorised is worse than a
question.
Some programs are shells in disguise
Those interpreter and launcher shapes are enforceable. A different, open-ended class cannot be exhaustively classified: many ordinary programs will run an arbitrary command if asked the right way.
git: hooks, aliases,core.pager,-ccargo:build.rs, runners and aliases in.cargo/config.tomlnpm: lifecycle scriptsmake: every recipetar:--checkpoint-action
allow: ["bash(git *)"] grants a shell, one subcommand away. crucible does
not pretend to police this; it would mean reimplementing each program's
argument grammar, wrongly. What it keeps instead is the rule text honest: you
wrote git *, and everything git can be told to do is what it covers.
The advice
Write deny for what must never happen; it holds in every mode and no other
list can qualify it. Write allow about exact commands such as
bash(git status) or bash(cargo test), and prefer answering y a few extra
times to a wildcard you would have to reason about. Leave everything you are
less sure of to the question. The question is the mechanism; the rules are
shortcuts through it.