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; a hook here is active only while this skill runs.

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

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.

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.