Claude Code / cookbook

Recipes that lean on what Claude Code has and other harnesses don't: subagents with their own context windows, hooks that decide rather than ask, and skills that package a workflow into one command. Hand-written, unlike the cheat sheet — the commands are real, the combinations are editorial.

Cheat sheet Cookbook Changes

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

  1. 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.
      }
    });
    
  2. 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/`.
    
  3. 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.

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

  1. 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
    
  2. 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.

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

  1. 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.
    
  2. 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"
    
  3. 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.

    /permissions
    

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.

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

  1. 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.
    
  2. 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.

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

  1. 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.
    
  2. 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.

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

  1. 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
    
  2. Check on the units without leaving the session. /tasks lists this session's background work, including subagents that have already finished.

    /tasks
    
  3. 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.

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

  1. 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
    
  2. 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
    
  3. 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.

Packaging a workflow

Turn a procedure you keep re-explaining into a skill intermediate

You have typed the same eight-step preamble into three sessions this month, and it goes slightly wrong in a different way each time.

on the sheet: skills

  1. Write the procedure once as a skill. Set disable-model-invocation: true when it should fire only on /name — a release procedure you did not ask for is a bug.

    .claude/skills/cut-release/SKILL.md
    ---
    name: cut-release
    description: Cut a release: changelog, version bump, tag, publish.
    disable-model-invocation: true
    allowed-tools: Read Edit Bash(git *) Bash(npm *)
    argument-hint: "<major|minor|patch>"
    ---
    
    Cut a **$0** release.
    
    1. Verify the working tree is clean and you are on `main`. Stop if not.
    2. Summarize commits since the last tag into `CHANGELOG.md`.
    3. Bump the version, commit, tag `v<version>`.
    4. Print the diff and **stop for review**. Do not push.
    
  2. Invoke it as a command. $0 interpolates the argument.

    # in-session
    /cut-release minor
    

A skill is a procedure under version control, so it gets reviewed and improved like code — instead of decaying in your muscle memory. The step that says stop for review is the one that matters: it puts the irreversible action behind a human.

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

  1. -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
    
  2. 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.

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

  1. 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
    
  2. 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.