{
  "id": "claude-code",
  "title": "Claude Code Cookbook",
  "lede": "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.",
  "updated": "2026-07-12",
  "categories": [
    { "id": "guardrails", "label": "Guardrails" },
    { "id": "parallel", "label": "Working in parallel" },
    { "id": "packaging", "label": "Packaging a workflow" },
    { "id": "headless", "label": "Headless & CI" }
  ],
  "recipes": [
    {
      "id": "hook-enforced-boundaries",
      "title": "Fence an agent into one directory — and mean it",
      "category": "guardrails",
      "difficulty": "intermediate",
      "when": "An agent should edit data but never code, and you want that *enforced* rather than politely requested in `CLAUDE.md`.",
      "sheetRefs": ["hooks", "skills"],
      "steps": [
        {
          "text": "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.",
          "code": {
            "lang": "text",
            "file": ".claude/hooks/data-only.mjs",
            "text": "import path from \"node:path\";\n\nlet raw = \"\";\nprocess.stdin.on(\"data\", (c) => (raw += c));\nprocess.stdin.on(\"end\", () => {\n  const file = JSON.parse(raw)?.tool_input?.file_path;\n  if (!file) process.exit(0);\n\n  const root = process.env.CLAUDE_PROJECT_DIR ?? process.cwd();\n  const rel = path.relative(root, path.resolve(root, file));\n\n  if (rel.startsWith(\"..\") || !rel.startsWith(\"data\" + path.sep)) {\n    console.error(`writes are limited to data/ — blocked ${rel}`);\n    process.exit(2); // 2 denies. 1 would be ignored.\n  }\n});"
          }
        },
        {
          "text": "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.",
          "code": {
            "lang": "markdown",
            "file": ".claude/skills/refresh-data/SKILL.md",
            "text": "---\nname: refresh-data\ndescription: Re-derive the dataset from upstream.\nallowed-tools: Read Write Edit WebFetch\nhooks:\n  PreToolUse:\n    - matcher: \"Write|Edit\"\n      hooks:\n        - type: command\n          command: node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/data-only.mjs\"\n---\n\nRefresh the dataset. You may only write under `data/`."
          }
        },
        {
          "text": "Test the deny path directly — feed the hook a synthetic event rather than driving a whole session.",
          "code": {
            "lang": "bash",
            "text": "echo '{\"tool_input\":{\"file_path\":\"src/index.js\"}}' \\\n  | CLAUDE_PROJECT_DIR=$PWD node .claude/hooks/data-only.mjs; echo \"exit=$?\"\n# exit=2  → denied, as intended"
          }
        }
      ],
      "why": "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.",
      "links": [
        { "label": "Hooks reference", "url": "https://code.claude.com/docs/en/hooks" }
      ]
    },
    {
      "id": "deny-by-default-tools",
      "title": "Give an automated run the smallest possible toolset",
      "category": "guardrails",
      "difficulty": "starter",
      "when": "You are running Claude unattended (cron, CI, a script) and want it to read and search but never write, install, or push.",
      "sheetRefs": ["flags", "modes"],
      "steps": [
        {
          "text": "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.",
          "code": {
            "lang": "bash",
            "text": "claude -p \"Summarize what changed in src/ this week and flag anything risky.\" \\\n  --allowedTools \"Read,Grep,Glob,Bash(git log *),Bash(git diff *)\" \\\n  --output-format json"
          }
        },
        {
          "text": "Reach for `--permission-mode acceptEdits` only when the run is *supposed* to write, and pair it with a hook that bounds where.",
          "code": {
            "lang": "bash",
            "text": "claude -p \"/refresh-data\" --permission-mode acceptEdits \\\n  --allowedTools \"Read,Write,Edit,WebFetch\" \\\n  --max-turns 80"
          }
        }
      ],
      "why": "`--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.",
      "links": [
        { "label": "CLI reference", "url": "https://code.claude.com/docs/en/cli-reference" },
        { "label": "Permission modes", "url": "https://code.claude.com/docs/en/permission-modes" }
      ]
    },
    {
      "id": "review-panel-subagents",
      "title": "Review a large diff with a panel of subagents",
      "category": "parallel",
      "difficulty": "intermediate",
      "when": "A diff is too large for one pass, and a single reviewer converges on the same three obvious findings instead of the subtle one.",
      "sheetRefs": ["agents", "parallel", "review"],
      "steps": [
        {
          "text": "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.",
          "code": {
            "lang": "markdown",
            "file": ".claude/agents/security-reviewer.md",
            "text": "---\nname: security-reviewer\ndescription: Reviews a diff for injection, authz, and secret-handling defects. Use for any change touching auth, input parsing, or shell invocation.\ntools: Read, Grep, Glob\nmodel: inherit\n---\n\nReview ONLY the diff you are given.\n\nReport each finding as `file:line — severity — what breaks`.\nA finding needs a concrete failure scenario: inputs, and the wrong\noutput or crash they produce. If you cannot name one, it is not a\nfinding — say so rather than padding the list."
          }
        },
        {
          "text": "Ask for them by name in one prompt so they run concurrently, each with its own context window.",
          "code": {
            "lang": "bash",
            "text": "claude \"Review the diff against origin/main with the security-reviewer,\nperf-reviewer, and api-reviewer subagents in parallel. Merge their\nfindings, drop duplicates, and sort by severity.\""
          }
        }
      ],
      "why": "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.",
      "links": [
        { "label": "Subagents", "url": "https://code.claude.com/docs/en/sub-agents" }
      ]
    },
    {
      "id": "explore-before-plan",
      "title": "Spend a subagent's context, not your own, on the search",
      "category": "parallel",
      "difficulty": "starter",
      "when": "Answering a question means grepping across dozens of files, and you want the conclusion without the file dumps eating your session.",
      "sheetRefs": ["agents", "session"],
      "steps": [
        {
          "text": "Delegate the sweep. The subagent reads widely and returns only what it concluded — the intermediate reads never enter your context.",
          "code": {
            "lang": "text",
            "text": "Use the Explore subagent to find every place we construct a\nStripe client, and report back the file paths plus which config\nkey each one reads. Don't fix anything yet."
          }
        },
        {
          "text": "Then plan against the summary, with the context you saved.",
          "code": {
            "lang": "bash",
            "text": "claude --permission-mode plan \"Given those call sites, plan the migration to a single factory.\""
          }
        }
      ],
      "why": "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.",
      "links": [
        { "label": "Subagents", "url": "https://code.claude.com/docs/en/sub-agents" }
      ]
    },
    {
      "id": "skill-as-checklist",
      "title": "Turn a procedure you keep re-explaining into a skill",
      "category": "packaging",
      "difficulty": "intermediate",
      "when": "You have typed the same eight-step preamble into three sessions this month, and it goes slightly wrong in a different way each time.",
      "sheetRefs": ["skills"],
      "steps": [
        {
          "text": "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.",
          "code": {
            "lang": "markdown",
            "file": ".claude/skills/cut-release/SKILL.md",
            "text": "---\nname: cut-release\ndescription: Cut a release: changelog, version bump, tag, publish.\ndisable-model-invocation: true\nallowed-tools: Read Edit Bash(git *) Bash(npm *)\nargument-hint: \"<major|minor|patch>\"\n---\n\nCut a **$0** release.\n\n1. Verify the working tree is clean and you are on `main`. Stop if not.\n2. Summarize commits since the last tag into `CHANGELOG.md`.\n3. Bump the version, commit, tag `v<version>`.\n4. Print the diff and **stop for review**. Do not push."
          }
        },
        {
          "text": "Invoke it as a command. `$0` interpolates the argument.",
          "code": {
            "lang": "bash",
            "text": "# in-session\n/cut-release minor"
          }
        }
      ],
      "why": "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.",
      "links": [
        { "label": "Skills", "url": "https://code.claude.com/docs/en/skills" }
      ]
    },
    {
      "id": "json-output-in-scripts",
      "title": "Use Claude as a step in a shell pipeline",
      "category": "headless",
      "difficulty": "starter",
      "when": "You want a script to act on what Claude concluded, not just print it to a terminal.",
      "sheetRefs": ["launch", "flags"],
      "steps": [
        {
          "text": "`-p` prints and exits; `--output-format json` makes the result parseable. Pull the answer out with `jq`.",
          "code": {
            "lang": "bash",
            "text": "verdict=$(claude -p \"Read the failing test output in /tmp/ci.log.\nReply with exactly one word: flaky, or real.\" \\\n  --allowedTools \"Read\" \\\n  --output-format json | jq -r '.result')\n\nif [ \"$verdict\" = \"flaky\" ]; then\n  echo \"retrying…\" && npm test\nfi"
          }
        },
        {
          "text": "For long runs, stream events instead of waiting for the final blob.",
          "code": {
            "lang": "bash",
            "text": "claude -p \"Audit every route for missing authz.\" \\\n  --output-format stream-json --verbose"
          }
        }
      ],
      "why": "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.",
      "links": [
        { "label": "CLI reference", "url": "https://code.claude.com/docs/en/cli-reference" }
      ]
    },
    {
      "id": "resume-long-work",
      "title": "Pick up exactly where you left off",
      "category": "headless",
      "difficulty": "starter",
      "when": "A migration spans days, and re-explaining the context each morning is both slow and lossy.",
      "sheetRefs": ["session"],
      "steps": [
        {
          "text": "Resume the last session in this directory with `-c`, or pick a specific one with `--resume`.",
          "code": {
            "lang": "bash",
            "text": "claude -c                       # continue the most recent session here\nclaude --resume                 # pick from a list\nclaude -c -p \"what's left?\"     # continue, headless, one question"
          }
        },
        {
          "text": "Branch a session with `--fork-session` when you want to try a second approach without losing the first.",
          "code": {
            "lang": "bash",
            "text": "claude --resume <session-id> --fork-session"
          }
        }
      ],
      "why": "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.",
      "links": [
        { "label": "CLI reference", "url": "https://code.claude.com/docs/en/cli-reference" }
      ]
    }
  ]
}
