When an agent has permission to read files, run shell commands, edit code, and ship PRs, a single line of instruction in the prompt is not enough. You need a check layer before the tool actually runs.
You give the agent a small task: fix validation in a module. To be safe it scans a few extra directories, opens a sensitive config file, then conveniently bundles some unrelated cleanup into the same PR.
At first glance everything looks fine. Tests still pass. But context has been polluted with junk files, secrets may have entered the transcript (the complete record of the conversation between you and the agent), and the review is diluted because the intended change and the "convenient" additions sit in the same PR.
cat .env — secret enters transcript.env — privacy-block asks userThe agent doesn't naturally become more careful. It's just that before it does anything, there is a layer that asks: should this action be allowed to run?
Hooks are the real blocking layer.
Guard rails don't make the model smarter. They check actions before the tool runs. Rules, hard-gates, and guard skills are additional orientation layers — useful, but still dependent on whether the model or user invokes them.
I. Concepts
A guard rail is the layer that stands between the agent and an action: blocking risky operations, warning on suspicious ones. It is like a railing on the edge of a staircase: it doesn't make you walk better, but it reduces the chance that a misstep becomes an accident. Unlike instructions in the prompt, i.e. prompt instructions, guard rails run at the tool-orchestration layer and do not depend on the model "remembering" them.
| Guard rail | Instruction | |
|---|---|---|
| Runs where | Harness, outside the model | In context, model reads it |
| Enforcement | Code blocks for real | Model voluntarily complies |
| Long context | Still runs | Easy to forget / slip |
| Model misreads | Still blocks (if no crash) | Drifts with the model |
| Modification | Touch code/config | Edit text and done |
Harness is the runtime layer wrapping the model. The model only decides "I want to read this file" or "I want to run this command"; it is the harness that receives the tool call, manages permissions, invokes hooks, lets the tool run or blocks it, then returns the result to the model. Hooks are small scripts the harness calls at fixed points in a tool call's lifecycle — on prompt receipt, before a tool, after a tool — to intervene and check. Because every real action goes through the harness, this is where guard rails attach.
For conceptual background, read: What is Harness Engineering? by Duy /zuey/.
For more detail on ClaudeKit Hooks, see VividKit Guides.
Hooks do not live inside the model. They live in Claude Code's lifecycle: settings.json decides which hooks are called, .ck.json/ENV decides runtime behavior, and hook output can allow, block, inject context, or write state/artifacts after a tool or session ends.
The key point: a pre-tool hook returns an exit code. Exit 0 lets the tool run; exit 2 blocks and pushes the reason back to the model. The three main injection points in the flow have corresponding technical names: UserPromptSubmit (on prompt receipt), PreToolUse (before a tool), PostToolUse (after a tool). Stop/SubagentStop run when a session or subagent ends, so they appear on the lifecycle map above.
An exit code is the number a process returns when it exits. Claude Code reads it to decide whether a tool runs:
exit 0 — OK, tool runs. stdout is read as JSON output.exit 2 — block. Discard stdout, push stderr back to the model as an error message.exit 1 (or other codes) — error but does NOT block. Reports the error and the tool still runs.Only exit 2 actually blocks. A hook that wants to enforce policy must use exit 2 precisely — accidentally using exit 1 out of Unix habit means the guard appears to be on but is actually open.
Hook exits 2, tool does not run. But if the hook crashes, Claude Code lets it through (fail-open). A bug in a hook silently disables that guard.
Adds text to context (rule, hard-gate). The model must comply on its own. Strength depends on the model.
.env, scan too broadly, or pull out-of-scope cleanup into a PRII. Reality in CK
After a fresh CK install, file-access guards live in PreToolUse hooks such as scout-block and privacy-block. If the Claude Code session is running with permission prompts suppressed, these hooks are still CK's primary blocking layer. But CK hooks are fail-open: if they crash or are disabled, the tool call may proceed.
| Group | Mechanism | Example | Enforcement |
|---|---|---|---|
| Block file/path | PreToolUse | scout-block | code · fail-open |
| Block wrong step | UserPromptSubmit | simplify-gate | code |
| Inject context | UserPromptSubmit | dev-rules-reminder | code |
| Keep names clean | Pre/Post/Stop | descriptive-name | code · warn |
| Hard-gate skill | XML markdown | <HARD-GATE> | instruction |
| Instruction rule | CLAUDE.md | review-audit | instruction |
| Guard skill | User invokes | ck:security-scan | user |
scout-block, privacy-block · Section 05-06simplify-gate, workflow gate · Section 07-08dev-rules-reminder injects text into prompt · Section 10descriptive-name · hook grid in Section 04<HARD-GATE> · Section 09CLAUDE.md rules are the injected content · Section 10ck:security-scan, ck:ship · Section 11| Situation | Handling layer |
|---|---|
Read node_modules/react/ | scout-block, exit 2 |
Read .env to check a key | privacy-block, exit 2 + approval prompt |
Glob **/*.ts at root | scout-block broad-pattern |
| Ship prompt when diff has ballooned | simplify-gate, if gate.enabled=true |
| Start coding before a plan/review exists | ck:cook HARD-GATE |
| Change a threshold the user has already confirmed | review-audit rule: ask before changing |
| New file with an ambiguous name | descriptive-name, PreToolUse(Write) |
| Plan uses wrong format for links/text | plan-format-kanban, PostToolUse(Edit/Write/MultiEdit) |
| Two teammates touching the same file | team-coordination rule, Agent Team only |
Two config layers determine which guards run: settings.json attaches hooks to Claude Code's lifecycle; .ck.json enables/disables individual hooks and adjusts thresholds.
# Global scope
~/.claude/settings.json
~/.claude/.ck.json
~/.claude/hooks/*.cjs
# Project scope
.claude/settings.json
.claude/.ck.json
.claude/hooks/*.cjs
The snippet below illustrates just one slice of PreToolUse from a fresh CK install: CK provides scout-block and privacy-block, then wires them into the lifecycle so Claude Code calls them before a tool runs. The full hook config on an installed machine has more lifecycle events; the place to check is Claude Code's settings.json.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Glob|Grep|Read|Edit|Write",
"hooks": [
{ "command": "node \".claude/hooks/scout-block.cjs\"" },
{ "command": "node \".claude/hooks/privacy-block.cjs\"" }
]
}
]
}
}
settings.json is the lifecycle map: which hook runs when the user submits a prompt, which is called before a tool runs, which handles things after a tool finishes. For a global install the CLI rewrites relative commands in the template to the form node "$HOME/.claude/hooks/scout-block.cjs"; for a project install, paths usually stay as .claude/hooks/....
Don't collapse everything into "on by default / off by default". The state of a hook has three distinct layers:
| Label | Meaning during audit |
|---|---|
script file | The .cjs file exists in .claude/hooks/. Having the file does not mean the hook is running. |
wired | settings.json has attached the script to a Claude Code lifecycle event. Without wiring, Claude Code never calls that hook. |
runtime flag | Once the hook is called, the code inside reads .ck.json/DEFAULT_CONFIG/ENV to decide whether to proceed. Some guards also have sub-switches such as privacyBlock or simplify.gate.enabled. |
The table below is a snapshot of the hook list for stable claudekit-engineer@2.19.1. Update 2026-06-09: claudekit-engineer@2.19.2-beta is preparing to remove task-completed-handler and teammate-idle-handler; when upstream ClaudeKit releases a new version, this list may become 14 hooks.
session-initwiredProject/env setup on startup, resume, clear, compact.
usage-quota-cache-refreshwiredDoes not read its own hook flag; runs when wired.
simplify-gategate offScript is called, but simplify.gate.enabled defaults to false.
dev-rules-reminderwiredInjects dev rules/context.
subagent-initwiredInjects context for subagent.
descriptive-namewiredPrompts meaningful file/script naming.
scout-blockwiredBlocks heavy dirs and overly broad globs.
privacy-blockwiredBlocks secret paths, requests approval.
plan-format-kanbanwiredDoes not read its own hook flag; warns on plan format.
session-statewiredWrites session/task state.
cook-after-plan-reminderwiredNo dedicated key in DEFAULT_CONFIG.hooks, but isHookEnabled() only disables when the flag is false.
workflow-artifact-gatenot wiredOpt-in for artifact gate: hook must be wired and flag/gate config must be enabled.
task-completed-handlerremovingAgent Teams task completed; do not treat it as durable after 2.19.1.
teammate-idle-handlerremovingAgent Teams teammate idle; do not treat it as durable after 2.19.1.
team-context-injectnot wiredNo dedicated key in DEFAULT_CONFIG.hooks; only meaningful when the workflow/team layer calls it.
usage-context-awarenessnot wiredUsage/context injection hook; not in settings.json template.
Easy to miss: isHookEnabled() only disables when hooks.<name> is false. A missing key is usually treated as enabled. But some guards also have their own switches: privacy-block still reads the legacy key privacyBlock=false, while simplify-gate has simplify.gate.enabled and ENV CK_SIMPLIFY_DISABLED=1.
For quick adjustments, edit .ck.json at the scope you want to affect. Project config takes priority over global when a key is explicitly set; missing keys continue to inherit/default. For hooks already wired in settings.json, setting false turns them off; setting true re-enables them after a higher scope has disabled them. The hook name in the Hook column is also the key used in hooks.<name>.
// .claude/.ck.json or ~/.claude/.ck.json
{
"hooks": {
"scout-block": false,
"privacy-block": true
},
"privacyBlock": true,
"simplify": {
"gate": {
"enabled": true
}
}
}
| Goal | Where to edit | Note |
|---|---|---|
| Disable a wired hook | .ck.json {"hooks":{"scout-block":false}} | Hook script remains, but runtime lets it through. |
| Re-enable a hook disabled globally | project .ck.json {"hooks":{"scout-block":true}} | Explicit local key overrides global. |
| Disable privacy guard | .ck.json {"hooks":{"privacy-block":false}}.ck.json (legacy key) {"privacyBlock":false} | privacyBlock=false is the legacy key but still takes effect. |
| Enable simplify-gate to actually block | .ck.json {
"hooks": {
"simplify-gate": true
},
"simplify": {
"gate": {
"enabled": true
}
}
} | Without simplify.gate.enabled=true, the gate's blocking mode is not active. |
| Disable simplify-gate for a session/scope | settings.json {
"env": {
"CK_SIMPLIFY_DISABLED": "1"
}
} | Env override is stronger than config gate. |
| Enable workflow-artifact-gate | settings.json wire UserPromptSubmit / PreToolUse(Bash).ck.json {"hooks":{"workflow-artifact-gate":true}} | Fresh install does not wire it by default, so editing <code>.ck.json</code> alone is not enough. |
To know whether a hook gets triggered, look at the lifecycle event, not the file name. The same hook script only runs when the event/matcher in settings.json matches the current action. The table below is a quick-check for each hook; not wired hooks need to be wired first, or called via manual CLI if the script supports it.
| Hook | Manual trigger | Notes |
|---|---|---|
session-init | Open, resume, clear, or compact a Claude Code session. | SessionStart(startup|resume|clear|compact). |
usage-quota-cache-refresh | Open a session, send a new prompt, or update a Task/Todo. | Cache usage; does not read its own hook flag. |
simplify-gate | Send a prompt with ship/merge/pr/deploy/publish intent when diff is large enough. | Requires simplify.gate.enabled=true; does not block by default. |
dev-rules-reminder | Send a new prompt. | UserPromptSubmit; injects rules with TTL. |
subagent-init | Start a subagent via Task/agent flow. | SubagentStart. |
descriptive-name | Let Claude attempt to create a file with Write. | PreToolUse(Write); prompts for a clear file name. |
scout-block | Let Claude read node_modules, dist, or a too-broad glob. | PreToolUse on Bash/Glob/Grep/Read/Edit/Write. |
privacy-block | Let Claude read .env, a key file, or a secret path. | PreToolUse; also checks privacyBlock. |
plan-format-kanban | Let Claude Edit/Write/MultiEdit a plan file. | PostToolUse; warns on format, does not read its own hook flag. |
session-state | Update a Task/Todo, end a subagent, or end a turn. | PostToolUse, SubagentStop, Stop. |
cook-after-plan-reminder | Let a Plan subagent finish. | SubagentStop(Plan); missing key is still enabled. |
workflow-artifact-gate | Wire into UserPromptSubmit/PreToolUse(Bash), or run the script with --stage. | Fresh install does not auto-trigger hook mode. |
task-completed-handler | Complete a task in Agent Teams. | TaskCompleted; preparing removal in claudekit-engineer@2.19.2-beta. |
teammate-idle-handler | Let a teammate in Agent Teams finish their work and go idle. | TeammateIdle; preparing removal in claudekit-engineer@2.19.2-beta. |
team-context-inject | Wire into SubagentStart, then start a team subagent. | Script is meaningful when the agent id belongs to a team. |
usage-context-awareness | Wire into the desired event, or use the hook cache refresh already wired. | Legacy wrapper around usage quota cache refresh. |
// ~/.claude/settings.json or .claude/settings.json
{
"env": {
"CK_SIMPLIFY_DISABLED": "1"
}
}
Agents often read node_modules/, glob **/*.ts at root (glob is a path-matching pattern; **/*.ts means every .ts file in every subdirectory), or cat dist/index.js. Each time that's tens of thousands of tokens stuffed into context, driving up cost and degrading the quality of subsequent turns. scout-block.cjs registers PreToolUse and runs before every Read/Bash/Glob/Grep.
# Baseline .ckignore — processed by pattern-matcher.cjs
node_modules
dist
build
.next
.nuxt
__pycache__
.venv
venv
vendor
target
.git
coverage
Build commands are allowed through. Commands such as npm build, go build, make, docker build still run, even when the build process touches node_modules or dist. Blocking this group would cause CK's ship/test flows to stumble at the build step.
Fail-open: parse errors all result in exit 0, letting the tool through. A bug in the hook silently disables that guard layer.
Agent touches .env, id_rsa, *.pem, credentials.yaml — secrets leak into the transcript. Once in context, they can be logged, quoted in reviewer output, or pasted into a PR.
@@PRIVACY_PROMPT_START@@
{ "type": "PRIVACY_PROMPT", "question": {...}, "options": [...] }
@@PRIVACY_PROMPT_END@@
Claude parses JSON and calls AskUserQuestion. If the user approves, the hook message instructs reading the file through an approved path, e.g. cat ".env" after asking; the code also supports the APPROVED: prefix. The key point: explicit approval is required — the model must not be allowed to self-interpret consent.
privacy-block lets Bash through, only warning. Anyone with permission to run Bash can read secrets without going through the approval flow.
Privacy-block only blocks READs. Rotate the credential immediately, audit conversation logs, check git history to see whether the secret was committed.
A small task can balloon into a PR touching too many files: fix validation, add cleanup, change formatting, tweak a few nearby helpers. When the agent says "OK ship", simplify-gate.cjs registers UserPromptSubmit, reads git diff HEAD, and only acts when the prompt contains: ship merge pr deploy publish.
Defaults from simplify.threshold: total added+removed lines, touched-file count, and the largest added-line count in one file. The gate only blocks when simplify.gate.enabled=true; these values can be overridden in .ck.json.
hooks.simplify-gate = true only tells Claude Code to call the script. To have the script actually block PRs that have ballooned past scope, you must also enable simplify.gate.enabled = true. A fresh install does not set this flag, so the gate only does a light check and does not block.
// project .ck.json — actually enable
{ "simplify": { "gate": { "enabled": true } } }
matchedSeverity() skips "don't ship" and "ship on" → "ship on Friday" also passes through. To disable the gate for all sessions at that scope, set CK_SIMPLIFY_DISABLED=1 in settings.json under the env key.
Full pipeline → each phase leaves a JSON file recording its decisions, like a receipt after every step. This hook is designed to attach to UserPromptSubmit + PreToolUse(Bash), but is not in the hook set that runs out of the box after a fresh install; you must opt in to use it.
| Artifact | Phase | Content |
|---|---|---|
context-snippets.json | scout/plan | Code snippets read |
risk-gate.json | predict | High-risk flag, auto-stop |
verification.json | fix/cook | 5-point checklist |
review-decision.json | code-review | Reviewer verdict |
adversarial-validation.json | adversarial | Adversarial pass |
This gate has two processing levels. At high-risk steps such as ship, push, PR, or deploy, the hook can stop the flow immediately and return the reason to the model (emitBlock()). At lighter steps such as finalize or commit, the hook lets the flow continue but injects a warning into context for the model to self-correct (emitSoft()).
workflow-artifact-gate is the strongest artifact-checking layer, but a fresh install does not call it automatically. To use it, wire the hook in settings.json and enable the config in .ck.json; without this step, ship/push/PR/deploy is not gated.
Hooks only block tool calls. Some failures are not tool calls but sequence errors: coding before planning, fixing before scouting. CK embeds <HARD-GATE> in skill markdown.
<HARD-GATE> Do NOT write implementation code until a plan exists and has been reviewed. Exception: --fast skips research but still requires a plan step. User override: if user explicitly says 'just code it', respect their instruction. </HARD-GATE>
ck:cook and ck:fix each have 4 hard-gates (plan/scout-first, exact-req or root-cause, no-side-effects).
The word "hard" is easy to misread — it does not block via code. It is an instruction in the prompt. If the model ignores it, there is no exit 2 to block. The XML wrapping creates a stronger signal than a plain rule, making it harder for the model to rationalize taking a shortcut.
dev-rules-reminder.cjs registers UserPromptSubmit and injects rules text into every prompt. Two layers need to be distinguished here: the hook is the context-injection mechanism, while the rules in CLAUDE.md are the content being injected. TTL is 5 minutes keyed on (sessionId, baseDir) to avoid burning tokens.
| File | Guards against | Characteristic |
|---|---|---|
review-audit-self-decision | Audit reversing a confirmed decision | verified sticky |
development-rules | Skip tests, fake data | YAGNI/KISS/DRY |
team-coordination-rules | Two agents editing the same file | ownership glob |
commit-messages | Long commit body | single-line |
orchestration-protocol | Pass full history | context isolation |
Rules are text in context, not code. Claude can drift if the user's prompt overrides them. On the other hand, they are very easy to add and edit. Better suited to style conventions than security guards.
| Skill | When to use | What it catches |
|---|---|---|
ck:security-scan | Pre-release | Secret, CVE, SQLi, XSS, path traversal |
ck:predict | Before risky feature | 5 personas, GO/CAUTION/STOP |
ck:scenario | Pre-impl | 12-dimension edge case sweep |
ck:ship | Pre-PR | Stop on test fail, never force-push |
code-reviewer agent runs a 9-item checklist: concurrency, error boundary, API contract, backwards compat, input validation, auth/authz, N+1, data leak, fact-check. Final guard before merge.
III. Limits & practices
scout-block/privacy-block, not a separate permissions.deny list for secrets/heavy dirsAn out-of-control agent can call thousands of Read/Glob/Bash operations. CK hooks do not count tool calls and do not self-stop when quota is nearly exhausted. Quota/cost limits live in Claude Code, the provider, or account billing — not in these guard rails.
gate.enabled = true.ckignoresettings.json currently attaching to which lifecycle events?.ck.json disabled? (outer + inner config).ckignore override? Is node_modules !-allowlisted?| Term | Short definition |
|---|---|
| harness | The orchestration layer between model and machine: sends tool commands, manages permissions, runs hooks. Where guard rails attach. |
| hook | A small script the harness calls at fixed points in the prompt/tool/session lifecycle to intercept, check, or write state. |
| lifecycle event | The points where hooks are called: UserPromptSubmit (prompt received), PreToolUse (before tool), PostToolUse (after tool), Stop/SubagentStop (session/subagent ends). |
| exit code | The number a process returns on exit. 0 allows, 2 blocks, 1 reports an error but does not block. |
| fail-open | When a hook errors/crashes, the harness lets the tool run instead of blocking. Guard silently disables. |
| transcript | The complete record of the conversation between the user and the agent in a session. |
| context | The information space the model "sees" when reasoning. Full of junk files → higher cost, lower quality. |
| glob | A path-matching pattern. **/*.ts = every .ts file in every subdirectory. |
| secret | Sensitive information: API keys, private keys, credentials. Leaking into the transcript is a risk. |
| LOC | Lines of code — line count, used to measure diff size. |
| artifact | JSON file each pipeline phase leaves behind, recording its decisions for the next phase to check. |
When an agent has broad permissions, don't conclude "CK is installed so we're safe". Before trusting a guard rail, check two places: which hooks settings.json is wiring, and what .ck.json is enabling or disabling.
Each layer protects against a different kind of risk: hooks block tool calls, rules nudge behavior, hard-gates hold the workflow, guard skills only run when invoked. Knowing which layer is running and which is just an instruction helps you audit the right place — before a bug slips into a PR and makes it to PROD.
If you are building a guardrails system, ClaudeKit is a useful set of applied patterns to study. If you are considering buying ClaudeKit so you can use the hooks, skills, and workflow guard rails as a ready kit instead of assembling each layer by hand, this referral link gives you 20% off.
Get 20% off