Guardrails
Fence an agent into one directory — and mean it intermediate
An agent should edit data but never code, and you want that enforced rather than politely requested in CLAUDE.md.
on the sheet: hooksskills
-
Write a PreToolUse hook. It receives the tool call as JSON on stdin, and exit code 2 blocks it — stderr goes back to Claude as the reason, so it can correct course rather than just fail.
.claude/hooks/data-only.mjs
import path from "node:path";
let raw = "";
process.stdin.on("data", (c) => (raw += c));
process.stdin.on("end", () => {
const file = JSON.parse(raw)?.tool_input?.file_path;
if (!file) process.exit(0);
const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
const rel = path.relative(root, path.resolve(root, file));
if (rel.startsWith("..") || !rel.startsWith("data" + path.sep)) {
console.error(`writes are limited to data/ — blocked ${rel}`);
process.exit(2); // 2 denies. 1 would be ignored.
}
});
-
Scope it to the skill it guards by declaring it in that skill's frontmatter. A hook in settings.json polices every session from the start; a hook declared here registers the moment this skill is invoked and then keeps running for the rest of the session (add once: true on the hook if you only want it to fire a single time).
.claude/skills/refresh-data/SKILL.md
---
name: refresh-data
description: Re-derive the dataset from upstream.
allowed-tools: Read Write Edit WebFetch
hooks:
PreToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: node "${CLAUDE_PROJECT_DIR}/.claude/hooks/data-only.mjs"
---
Refresh the dataset. You may only write under `data/`.
-
Test the deny path directly — feed the hook a synthetic event rather than driving a whole session.
echo '{"tool_input":{"file_path":"src/index.js"}}' \
| CLAUDE_PROJECT_DIR=$PWD node .claude/hooks/data-only.mjs; echo "exit=$?"
# exit=2 → denied, as intended
The permission system asks; a hook decides. Anything you would otherwise write in CLAUDE.md as a "please don't" is a rule the model can rationalize its way past under pressure — so encode it where it cannot. Note the exit codes are unintuitive: 2 blocks, 1 is treated as a non-blocking error and the action proceeds anyway.
Hooks reference
Give an automated run the smallest possible toolset starter
You are running Claude unattended (cron, CI, a script) and want it to read and search but never write, install, or push.
on the sheet: flagsmodes
-
Allowlist the tools instead of denying the dangerous ones — a denylist is a list you will always be one entry behind on. --allowedTools accepts per-command Bash scoping.
claude -p "Summarize what changed in src/ this week and flag anything risky." \
--allowedTools "Read,Grep,Glob,Bash(git log *),Bash(git diff *)" \
--output-format json
-
Reach for --permission-mode acceptEdits only when the run is supposed to write, and pair it with a hook that bounds where.
claude -p "/refresh-data" --permission-mode acceptEdits \
--allowedTools "Read,Write,Edit,WebFetch" \
--max-turns 80
--dangerously-skip-permissions is the flag everyone reaches for and it is almost never what you want in automation: it removes the last thing standing between a confused agent and your working tree. An explicit allowlist plus a --max-turns ceiling costs one line and bounds the blast radius.
CLI reference · Permission modes
Give `auto` mode a boundary that survives compaction intermediate
You're running a long stretch of unattended work in auto mode and told Claude mid-conversation not to touch production — that holds right up until compaction summarizes away the message that said so.
on the sheet: modes
-
A boundary you state in chat blocks matching actions immediately, even ones the classifier's defaults would otherwise wave through. But it isn't stored as a rule — the classifier just re-reads it from the transcript on every check.
Don't push anything, to any branch, for the rest of this session — I'll review and push myself.
-
For the one thing that actually must never happen, back it with a deny rule instead. A rule isn't part of the transcript, so compaction can't lose it.
claude --permission-mode auto \
--disallowedTools "Bash(git push *)" \
"Audit infra/ for configs that violate our tagging policy and write findings to infra-audit.md"
-
If the classifier blocks something you didn't expect, /permissions lists it under the Recently blocked tab, where you can approve it so Claude may retry — that's how you find out it caught the right thing, instead of assuming.
A conversational boundary and a deny rule look interchangeable in the moment — both stop the action — but only one survives your context compacting out from under it. auto mode earns its lower prompt volume specifically because you can make that trade on purpose: soft boundaries for things you're fairly confident about, --disallowedTools for the one action that must never run no matter what the classifier infers.
Permission modes
Working in parallel
Review a large diff with a panel of subagents intermediate
A diff is too large for one pass, and a single reviewer converges on the same three obvious findings instead of the subtle one.
on the sheet: agentsparallelreview
-
Define reviewers with disjoint mandates — the point is perspectives that can't collapse into each other. Give each only the tools it needs; a reviewer that can't write can't "helpfully" fix things mid-review.
.claude/agents/security-reviewer.md
---
name: security-reviewer
description: Reviews a diff for injection, authz, and secret-handling defects. Use for any change touching auth, input parsing, or shell invocation.
tools: Read, Grep, Glob
model: inherit
---
Review ONLY the diff you are given.
Report each finding as `file:line — severity — what breaks`.
A finding needs a concrete failure scenario: inputs, and the wrong
output or crash they produce. If you cannot name one, it is not a
finding — say so rather than padding the list.
-
Ask for them by name in one prompt so they run concurrently, each with its own context window.
claude "Review the diff against origin/main with the security-reviewer,
perf-reviewer, and api-reviewer subagents in parallel. Merge their
findings, drop duplicates, and sort by severity."
Subagents each get their own context window, so three reviewers read the whole diff without competing for one budget — and the parent sees only the merged findings, not three transcripts. The disjoint mandates are what actually buy you coverage: three general-purpose reviewers would just agree with each other.
Subagents
Spend a subagent's context, not your own, on the search starter
Answering a question means grepping across dozens of files, and you want the conclusion without the file dumps eating your session.
on the sheet: agentssession
-
Delegate the sweep. The subagent reads widely and returns only what it concluded — the intermediate reads never enter your context.
Use the Explore subagent to find every place we construct a
Stripe client, and report back the file paths plus which config
key each one reads. Don't fix anything yet.
-
Then plan against the summary, with the context you saved.
claude --permission-mode plan "Given those call sites, plan the migration to a single factory."
Context is the scarce resource in a long session, and search is what burns it fastest — a hundred file reads to answer one question. Pushing the search into a subagent means you pay for the answer, not the transcript.
Subagents
Fan a codebase-wide change out to N PRs, then gate each with a background review intermediate
A change touches the same pattern across dozens of files — a rename, a dependency swap — and reviewing it as one giant diff would bury the interesting case among the mechanical ones.
on the sheet: parallelreview
-
Hand the whole migration to /batch. It researches the codebase, proposes 5–30 independent units, and — once you approve the plan — spawns one background subagent per unit, each in its own worktree, each opening its own PR.
/batch migrate every fetch() call in src/ to the new apiClient wrapper
-
Check on the units without leaving the session. /tasks lists this session's background work, including subagents that have already finished.
-
Review each resulting PR in the background instead of pulling every diff into your own context — /code-review runs as its own subagent and can post its findings straight onto the PR as inline comments.
/code-review medium --comment 142
/batch's worktree-per-unit isolation is what makes this safe: the units can't stomp on each other's edits, and /tasks gives you a status check that doesn't cost a subagent transcript to read. Reviewing PR-by-PR with /code-review --comment keeps each review sized to one unit, so a subtle bug in unit 14 isn't drowned out by the other 29 mechanical diffs.
Commands reference · Subagents
Carry `.env` into every worktree Claude spins up starter
--worktree, /batch, and isolation: worktree subagents all start from a fresh checkout — no .env, no local secrets — so the first npm run dev or test run fails before Claude even gets to your actual task.
on the sheet: flagshooks
-
Drop a .worktreeinclude file at the repo root, in .gitignore syntax. Only files that are both gitignored and match a pattern get copied — tracked files are never duplicated.
.worktreeinclude
.env
.env.local
config/secrets.json
-
That's it — it applies automatically to every worktree Claude Code creates with git: a --worktree session, a /batch unit, an isolation: worktree subagent. No flag or hook to wire up. Gitignore the worktree directory itself so its contents don't show up as untracked files back in the main checkout.
echo '.claude/worktrees/' >> .gitignore
-
The one place this doesn't reach: if you've replaced worktree creation with your own WorktreeCreate hook (for SVN, Perforce, or another non-git VCS), .worktreeinclude is skipped entirely — the hook owns the whole checkout, so copy those files inside the hook script instead.
Every parallel-work recipe here — /batch, subagent worktree isolation — inherits this gap for free: a worktree is a fresh checkout, not a clone of your working tree. .worktreeinclude closes it once at the repo level instead of re-solving it per session, and knowing where it doesn't apply (hook-based non-git worktrees) saves you from assuming it silently handles a case it doesn't.
Worktrees · Hooks reference
Headless & CI
Use Claude as a step in a shell pipeline starter
You want a script to act on what Claude concluded, not just print it to a terminal.
on the sheet: launchflags
-
-p prints and exits; --output-format json makes the result parseable. Pull the answer out with jq.
verdict=$(claude -p "Read the failing test output in /tmp/ci.log.
Reply with exactly one word: flaky, or real." \
--allowedTools "Read" \
--output-format json | jq -r '.result')
if [ "$verdict" = "flaky" ]; then
echo "retrying…" && npm test
fi
-
For long runs, stream events instead of waiting for the final blob.
claude -p "Audit every route for missing authz." \
--output-format stream-json --verbose
Constrain the output shape in the prompt ("exactly one word"), not just the format flag — --output-format json guarantees you get valid JSON, never that .result contains what your if statement expects.
CLI reference
Pick up exactly where you left off starter
A migration spans days, and re-explaining the context each morning is both slow and lossy.
on the sheet: session
-
Resume the last session in this directory with -c, or pick a specific one with --resume.
claude -c # continue the most recent session here
claude --resume # pick from a list
claude -c -p "what's left?" # continue, headless, one question
-
Branch a session with --fork-session when you want to try a second approach without losing the first.
claude --resume <session-id> --fork-session
Forking is the underused one: it turns "should I try the risky refactor?" into a cheap experiment, because the timeline you branched from is still intact if it doesn't work out.
CLI reference