Skip to content

Gates

A rule that matters becomes a gate. Enforcement sits at the tool level — a hook, a permission, a test case — so the model is not asked to remember it.

Component Count What it is
Agents 14 Thin triggers — who owns a domain and when they fire
Skills 40 The method, written once, applied by whoever needs it
Commands 18 /crew-brainstorm · /crew-plan · /crew-review · /crew-ship · /crew-handoff · /crew-update · /crew-doctor · /crew-gates · /crew-skill · /crew-studio
Hooks 25 The gates, plus session measurement and routing
Discipline 1 Principles, workflow, Definition of Done, prohibitions — imported by your CLAUDE.md
Hook Role
prompt-approval.sh In auto and dontAsk, records the command you type, /crew-approve commit (or push, commit+push), as the approval, tied to what is staged, to HEAD and to the session
guard-schedule.sh Refuses a tool call that schedules a prompt (CronCreate, ScheduleWakeup, RemoteTrigger, an MCP tool named for a trigger, a schedule or a cron) when that prompt is an approval: an approval is what you type, never what a session schedules for itself
guard-question.sh Lets the approval question (one click in auto) reach you and not a hook: it answers ask on the fixed question only, refuses any other wording or a question with answers already in it, and closes it where a PermissionRequest hook may be wired
guard-agent-model.sh Reads the task card a crew-* agent is called with (files, kind of change, verify command; on three lines or on one, files: … · change: … · verify: …, and a kind may carry a note in parentheses) and refuses a call whose model does not fit: no card or no model; critical work (a migration, security, architecture or unknown-cause change, or a file on a critical path) below opus; test-writing, audits and reviews below sonnet; a feature, a known fix or a refactor below sonnet (except the commit agent); work with no verify command on haiku (except a text change and the commit agent); a text change whose every file is prose (.md, .txt, .rst, .adoc) on a critical path below sonnet, so such a change is written on sonnet and audited on opus; a card whose verify failed, on the same or a lower model; critical work with a verify command in the background. A call above the card’s floor runs on the floor (the gate rewrites its model) unless the card adds above: <why>. Outside the crew, a call asking for opus or fable, or a general-purpose call that names no model in a session not on haiku, runs on sonnet. fable needs CREW_ALLOW_FABLE=1; CREW_MODEL_ROUTING=off turns it off
agent-outcome.sh When a crew agent stops, runs its card’s verify command: red once, the agent is kept running to fix it; red again, the session is told to repeat the task once on the same model at effort high when it ran below, otherwise one model up. Writes one line per call to .claude/state/model-outcomes.tsv, raises a class’s floor when its first try fails too often, and lists the project’s critical paths. Not a gate: it cannot undo what an agent wrote. The verify command runs only when you allowed it by name (a verify line in .claude/crew-model-rules, which /crew-allow-verify <command> adds and, with nothing after it, lists the ones refused; or a Bash(...) rule in permissions.allow) or, in auto and bypassPermissions, when it is a known test or build runner with no options. Known limits: a test runner runs code the agent wrote (test files, package.json scripts, conftest.py, a Makefile), so the list limits the command and not what the tests do; a failed card stays failed until the card changes; the shell rule for the routing files goes by their names; the list of critical paths is written at the first session, not at install; managed policy is read from the managed settings file only, not from macOS MDM, the Windows registry or the server. A raised floor is lowered by /crew-loosen <agent> <change> <risk>, a command only you can send: one model, once for each raise. Model routing applies to crew-* agents only: Explore is held to haiku and general-purpose to sonnet (a line above: <why> in the task keeps a higher model), a project’s own agents run on their own model or the session’s, and none of them is in the record. The gate holds a floor, not a ceiling: nothing stops ordinary work from being given to Opus. The shell rule that keeps a session from writing that record can be passed by a name built from pieces, an interpreter or a parent-folder move; such a write can add a lowering, not only remove one
refuter-ledger.sh When a crew-refuter agent stops, records its verdict on a finding in .claude/state/crew-refuter.tsv, from the agent’s own task and answer, and only when the two name the same finding and lens. findings.sh write --verified holds a findings document to that ledger; the session cannot write it
loop-guard.sh After each Bash, Edit and Write call, counts the turn: calls, edits per file, failures and how often one failure repeats. When 20 calls, 4 edits to one file or edits to 3 files, 6 failures and one failure making up at least a fifth of them all hold, it fires once per prompt. In this release it only records that: one line of counters (no command, path or error text) in .claude/state/loop-guard.jsonl, and nothing reaches the session. CREW_LOOP_GUARD=on sends the session back to one falsifiable hypothesis instead; off turns it off. Never blocks
map-stale.sh After an Edit or Write to a file .crewforth/crew-map.json cites, says once per agent, as a fact, that the map was scanned before the edit and which check tells whether it still matches the tree. When .crewforth/contracts.md cites the file, it adds the project’s rules that live there, once per rule and agent, and at Stop tells the user which rules the session’s edits touched. Silent with no map, for a file the map does not cite, and with CREW_MAP_STALE=off. Never blocks
turn-check.sh When the main thread’s turn ends, runs the Fast check (files): command CLAUDE.md declares on the files that turn changed: edits made in the turn and the turn’s own commits, not files that were already dirty and not commits a pull brought in. Red hands the output back once so the turn fixes it; a command the permission rules do not allow is not run, and you are told once. Nothing declared: nothing runs. CREW_TURN_CHECK=off turns it off
route-hint.sh Names the owning agent alongside every prompt, so specialists run without you asking
guard-bash.sh Tool-level command gate: commit/push approval, review-before-commit, dependency-audit-before-a-lockfile-commit, destructive ops, remote-code-exec, hook tampering
guard-write.sh The same protection on the Write/Edit side — a gate you can silently delete is not a gate. It normalises the target path before matching it, so a gate file cannot be reached under a different spelling.
guard-commit-scan.sh Runs the real trace and secret scanners from PreToolUse, so the commit gate works where core.hooksPath cannot be set
guard-powershell.sh Sends Crewforth’s own scripts back to the Bash tool when the model tries them in PowerShell, where bash can be WSL’s and the script fails
context-usage.sh Reads the real token count from the transcript and injects it every turn
session-guard.sh Warns once at 75% context fill and once at 90% — never blocks a turn
precompact-snapshot.sh Just before a compaction, writes down the state of the work from the files and the gates: the branch, the uncommitted files, the last refusal in the gate log and the findings on record, at most ~2 KB, inside the repository’s git directory. It never blocks the compaction
session-rehydrate.sh Re-surfaces the handover after /compact or /clear, gives a compacted session back the state precompact-snapshot.sh wrote for it (the same session only), and tells a session which language the project was installed in
skill-trust.sh Names any skill or agent Crewforth never shipped and you never accepted
session-stats.sh Reports what the session actually did — failing tool loops, repeated prompts, interrupts. reflect and handoff read it, so a retrospective rests on the record rather than on recollection
session-update-check.sh Asks once, when a session opens, whether to update when a newer version is published — each edition compared against the channel that will deliver it. The lookup runs detached and at most daily, so an offline or proxied machine costs the session opening nothing; CREW_NO_UPDATE_CHECK=1 turns it off
worktree-provision.sh When a session enters a worktree made outside .claude/worktrees/ (git worktree add ../x), copies this checkout’s .claude/ into it, without the parts that belong to one checkout (worktrees/, state/, the review record, the gate log). .claude/ is gitignored, so such a worktree has none, and a session resumed there would load no Crewforth hook at all. When the copy cannot be made it refuses the move and says why
branch-fresh.sh When a session starts or resumes, compares the branch with origin/<base> as last fetched (Base branch: in CLAUDE.md, else origin’s default branch) and speaks only when it matters: merging the base would conflict, the branch was cut more than 3 days ago, or the base changed .claude/ or CLAUDE.md. It names git merge origin/<base> and changes nothing. A fetch for the next session runs in the background, never while you wait; CREW_BRANCH_FRESH_FETCH=off turns it off, CREW_BRANCH_FRESH=off the whole check

The other two hooks serve an experimental feature and do nothing until a repository switches it on.

Two git hooks — pre-commit and commit-msg — run the trace, secret, repo-bloat and private-path scans. The last one exists because a path that only lives on your machine reaches a shared repo by being pasted, not by being typed: it blocks your own $HOME automatically, and the internal project, client and host names only you can recognise come from a gitignored .private-terms.txt (.private-allowlist.txt is the escape). Every gate matches in the C locale, so a system locale such as Turkish, where i and I are not a pair, does not change what is caught; and a scan that cannot run stops the commit instead of passing it. The plugin edition ships all of these except skill-trust.sh, which decides what Crewforth owns from the kit-manifest.txt an installer writes and the plugin never creates, and worktree-provision.sh: a plugin’s hooks do not live in .claude/, and a plugin enabled for a repository also loads in its worktrees.

Left is the rule; right is the thing that refuses to let it slide.

Rule Enforced by
Commit and push need your approval, in every permission mode; staging and creating a branch are free guard-bash.sh raises a prompt only you can answer. In auto and dontAsk, where a prompt is answered by software, it accepts one thing: a message from you that is only the command /crew-approve. With nothing after it, it approves what the gate just refused in that session (a commit, a push, or both when both were refused), if that was within the last 30 minutes; spelled out it is /crew-approve commit, /crew-approve push or /crew-approve commit+push. Claude cannot run that command for you, cannot schedule it, and cannot write the note of what is waiting. prompt-approval.sh records it with the tree of what is staged, HEAD, the branch, the address its remote pushes to and the session; the call has to be that one git commit -m … or git push <remote> <branch> and nothing else, the record lasts 30 minutes and your next message ends it. When the refusal says the approval path is closed, the recording hook is not wired in that session (a worktree on an older Crewforth, for one): press Shift+Tab or commit in your own terminal, and doctor.sh names it. In auto there is also one fixed question (Commit only, Commit + push, Push only, No): agent-outcome.sh hands it to the session when crew-commit-agent is called, so /crew-ship asks it with the commit message, and a refused commit offers it too; Crewforth’s guard-question.sh answers ask on it, which a hook that fills in the answer cannot override, and your click is recorded the same way. A PermissionRequest hook can still answer a prompt before you do, so where one may be wired the question closes, and so does the gate’s prompt in default and acceptEdits: /crew-approve is then the one approval. It is looked for in your user, project and local settings, the managed-settings files, skill, agent and command front matter, and installed plugins; settings passed with --settings and managed settings delivered by a server cannot be read. Warp’s plugin hook, which only shows a notification, does not count: its entry and the scripts it runs are pinned by content, and a changed one counts again. A plain git pull that cannot make a commit runs in auto: git 2.33.1 or newer, none of pull.ff, pull.rebase, merge.ff=false or the branch’s own reconcile settings, and no -c, -C, --no-rebase or second branch; any other pull is asked. A here-document that cat or tee writes to the screen or to a document or data file is text: a git commit written in it asks for nothing. Fails closed under plan and bypassPermissions
A commit needs a clean review of the diff it is actually about guard-bash.sh compares git’s object id of the staged diff, and the HEAD it was reviewed against, with what crew-review-agent recorded when it cleared the change. A review of another diff — or of this one on another base — does not count, and there is no size exemption. A command that stages and commits in one call (git add b.txt && git commit) is refused when the add would bring in something not staged yet, because the gate reads the index before the command runs; a restore, reset, stash, checkout or merge beside a commit is refused outright
A commit that stages a lockfile needs a dependency audit of that lockfile change guard-bash.sh compares git’s object id of the staged lockfile diff (package-lock.json, uv.lock, go.sum, Cargo.lock and the other lockfiles of the supported stacks) with the record the dependency-audit skill writes once it has audited the change: clean, accepted (findings the user accepted by name) or unverified (the audit could not run, and the record says why). The record lives in the git directory, so it is never committed; code staged beside the lockfile keeps it valid, another lockfile change needs a new audit. The hook makes no network call, it only checks the record. Because a CVE is published without the lockfile changing, doctor.sh and a session start (at most once a week) say when the last audit is older than CREW_DEPS_AUDIT_DAYS (7 days; 0 turns it off); record.sh --periodic records an audit made with no lockfile staged and never lets a commit through
Destructive ops: reset --hard, checkout -- ., force push, rm -rf, clean -f, --no-verify, amend guard-bash.sh, blocked at the tool level. A git commit is read the way the shell and git read it, so -n, an abbreviated --no-verif or --amen, a flag behind a redirection or inside mis-paired quotes, and a commit run from another directory or under GIT_DIR / GIT_INDEX_FILE are refused as what they are. A git call whose subcommand the shell fills in (git com${z}mit, git "$c"), or git started through System.Diagnostics.Process in PowerShell, is refused: the gate must be able to read which git command it is. In PowerShell, arguments handed to Start-Process git or to a command held in a variable are judged as the git command they make. A wholly quoted argument of a command that does not run it (a commit or tag message, a gh pr, issue or release title or body, a claude -p prompt run where the call stands and with no option that changes its settings, a grep pattern, an echo that goes nowhere) is not read as a command. A command that names a gate file (a hook, settings.json, the rulebook, a git hook) passes when it reads the file, runs a hook script or stages it with git; any other command that names one is refused, whatever it is called. A command continued on the next line (a backslash, or a backtick in PowerShell) is read joined, as the shell runs it. A command that holds a commit and is larger than 32 KB is refused unread: write the message to a file and use git commit -F <file>. Any Bash or PowerShell call larger than 256 KB is refused unread too: a hook that runs out of time stops nothing, so the long content goes in a file. A recursive rm whose every target resolves strictly inside /tmp, $TMPDIR or the system temp folder passes; the temp root itself, a glob at its first level, .., a name the call does not set plainly, and rm behind sudo, xargs, find -exec or eval stay refused. On Windows a drive path written with backslashes (C:\…), $USERPROFILE and the temp root by name ($TEMP, $TMP, $TMPDIR) are refused like ~
Remote code execution and permission nukes: curl…|bash, world-writable chmod, dd of= guard-bash.sh, hard-blocked in every mode. A download read as data by a Python program written in the call (curl … | python3 -c '…') passes when the program imports only modules that cannot run text or reach the system (json, sys, re, collections and a few more) and names none of the ways Python runs or looks up a thing by a string; a bare | python3, any other import and every other interpreter stay refused
Disarming a gate — redirecting core.hooksPath or crewforth.hookschain, the project hook chain the shim runs on every commit (with git config, by writing .git/config or your own ~/.gitconfig, through an included file or a GIT_CONFIG_ variable), editing or deleting a hook, or rewriting the discipline the gates enforce guard-bash.sh (shell) + guard-write.sh (file edits). Both match the resolved path, so .. segments, doubled slashes, Windows separators and a symlinked parent all reach the same verdict as the plain spelling
No API key, token or private key reaches a commit pre-commit secret scan; every pattern carries its own test case
No machine-private path or internal name reaches a commit pre-commit private-path scan: your own $HOME automatically, plus a gitignored .private-terms.txt
No credential is read into the context — ~/.ssh/id_rsa, ~/.aws/credentials, *.pem, kubeconfig settings.json read-deny + guard-bash.sh
No AI-authorship trace or vendor template name in a commit pre-commit + commit-msg git hooks
No build artifact, vendored tree or oversized blob gets staged pre-commit repo-bloat scan
No commit quietly lowers the quality bar: a checker switched off where it fired, a test skipped or deleted, assertions taken out of a test that stays, a stub or an empty catch where the work should be pre-commit floor guard, across the supported stacks. Generated files and documentation are exempt; a genuine exception is a line in .floor-allowlist.txt, in the same commit, where review sees it
An unvetted skill or agent appearing in .claude/ is named, with a scanner verdict skill-trust.sh at session start
Always-on context stays lean smoke-test.sh byte budget per component
A running session never follows stale rules after an update context-usage.sh version comparison

Every rule carries cases for both halves: that it blocks what it must, and that it does not block its neighbours — chmod 755, rm -rf build, git checkout -- src/app.js. A gate nobody proved is not a gate, and a gate that fires on routine work gets worked around.

The gates stop accidents, not determined attempts. On a command line there is always a way around a pattern; if you need a real boundary, run Claude Code in a devcontainer or a VM. /crew-doctor tells you whether you have one.

The Bash guard appends a line to .claude/gate-log.tsv for each block, approval prompt and CLAUDE_GIT_OK pre-authorisation (BLOCK / ASK / ALLOW), and the gate-file write guard one for each block, with the section and the rule; the command is recorded only with CREW_GATE_LOG_CMD=1. It is on by default when the project’s .claude/ directory exists: in .claude/gate-log.tsv where git ignores it or the project is not a repository, and otherwise in the repository’s own .git/crewforth-gate-log.tsv, which is never committed. The model gate writes its refusals and notes (a model lowered to the card’s floor) to the same log. CREW_GATE_LOG=<path> sends it elsewhere and /dev/null turns it off. The commit scan refuses without writing a line. It is write-only and written after the verdict, so it cannot change one. Useful when you need to know whether a gate stopped something or the model simply never went there — those two leave identical traces.

When guard-bash.sh stops a destructive command, or asks about a commit or a push, it says what the command would have touched, in one line under the refusal or in the question:

Command The line
git reset --hard how many uncommitted changes to tracked files it discards, and with a target (HEAD~3) how many commits it drops from the branch
git clean -f… the same command as a dry run (-n, with its -d, -x, -X) and how many paths it lists
git push --force how many commits the remote branch has that this one does not, as of the last fetch
rm -r… the files under each of the rm’s own targets inside the project (never a word of the next command); a target outside it, or a pattern, is said and not counted
an asked commit the files and lines it takes
an asked push how many commits it sends, the first three by subject

It only reads (status, diff, rev-list, log, clean -n, find, at most 10 000 files), fetches nothing, and runs after the decision in a subshell of its own, so a preview that fails or ends its shell leaves the refusal or the question as it was. A Remove-Item in PowerShell gets no preview.

If you turn on Claude Code’s advisor, the model decides when to consult it, and nothing caps or forces a call. So it adds a second opinion, not a guarantee: no gate waits for it, a verify command still decides whether work passed, and a medium-or-higher finding still needs a fresh crew-refuter that has not seen the reasoning, which an advisor reading the whole conversation is not. What it costs is on the Sessions and cost page.

Read from the hooks when the site is built — the same inventory gate-report.sh derives — so a rule added to a hook appears here without anyone editing this page.

Rule
CLAUDE_GIT_OK pre-authorised session
a Claude Code session started from a command to continue this one (it could approve for it)
a command that hands a Claude Code session an approval as its prompt (only the user’s own message approves)
a commit made by another git command with no approval
a push form no approval covers
approval key set inside the command
commit/push approval prompt
commit/push under a mode that cannot prompt
commit/push with no matching approval from the user
the approval hook fed by a command (only Claude Code runs it, with the user’s message)
the approval record named in a command (only the user’s own message writes it)
the user’s own approval message covers this command
PowerShell call too large to read
PowerShell disk-level destructive command
PowerShell download-and-execute (… | iex)
PowerShell recursive force delete (Remove-Item -Recurse -Force)
PowerShell world-writable ACL (icacls /grant Everyone:F)
a command that opens another Claude Code session’s screen or socket (claude attach, claude agents, claude respawn, the messaging socket): it can answer that session’s questions, an approval included
a model routing record named in a command (the user and Crewforth’s hooks write it; to show it to the user, read it with the Read tool)
abbreviated git commit –amend
chmod world-writable (777/1777/666/o+w …)
cluster teardown (kubectl delete / helm uninstall)
dd of= (disk overwrite)
disk-level destructive command
forced git branch (-D / -f / -M / -C)
git -c core.hooksPath (disarms the git hooks)
git -c crewforth.hookschain (the project’s own hook chain, dropped or pointed elsewhere)
git call with an expansion in a command too large to read
git commit –amend
git commit argument the shell expands
git commit in a command too large to read
git commit inside a nested shell
git commit option the gate cannot read
git commit through a command the shell fills in
git commit under a GIT_ variable set in the command
git commit under a setting given in the command
git commit written so that it is not recognised
git config –edit (the configuration file opened for writing)
git config core.hooksPath (disarms the git hooks)
git config crewforth.hookschain (the project’s own hook chain, dropped or pointed elsewhere)
git config include.path (another file read as configuration)
git filter-branch/filter-repo
git rebase
git started through System.Diagnostics.Process
git subcommand the shell fills in
git update-index –add (bypasses .gitignore, same as git add -f)
hook skip (–no-verify)
hook skip by -n or an abbreviated –no-verify
in-place edit of a gate file
indirected write to a gate path (variable + redirect)
infrastructure destroy (removes every managed resource)
pipe-to-shell (curl|bash RCE)
reading a .env secret via the Bash tool
reading a private key / credential file via the Bash tool
redirect over a gate file
symlink pointing at the config directory (a gate path in two steps)
the refuter ledger named in a command (only Crewforth’s hook writes it)
unattended infrastructure apply (skips the tool’s only confirmation)
whole-tree revert (git checkout/restore over everything)
write/tamper of a gate file (hook/settings/.git-hooks)
a command that rewrites the index and commits
a command that stages and commits, with unstaged changes
commit after a change of directory
commit redirected at another worktree
commit takes content from the working tree
no review-pass record
review-pass does not match this diff
review-pass matches the staged diff
a command that stages and commits, with a lockfile changed
a staged lockfile with no audit record
the audit record does not match the staged lockfiles
the audit record matches the staged lockfiles
the lockfile list could not be loaded