Claude Code Advanced Patterns: Hooks, MCP, Parallel Agents, Worktrees, and Context
Half a year of running Claude Code, and the framing has shifted on me. Started out treating it like “VSCode + Cursor with a different vibe”; now it feels closer to managing a small team that can run jobs in parallel. This post is a sweep of the high-leverage features I actually depend on — hooks, MCP, subagents, worktrees, layered CLAUDE.md, when to
/clear. Not a doc rewrite. The lens is “what a user with opinions actually does”, and you should be able to lift any block straight into your own setup.
0. Map of the article
Eight modules, ordered from “do this first” to “feel this out”:
| # | Module | Type | One line |
|---|---|---|---|
| 1 | Hook system | Hands-on | Hang your own scripts on lifecycle events |
| 2 | Layered CLAUDE.md | Hands-on | A project manual the agent always carries |
| 3 | MCP server | Hands-on | Wrap any external capability into a callable tool |
| 4 | Parallel agents | Hands-on | Fan out subagents from one session |
| 5 | Worktree isolation | Hands-on | git worktree keeps multiple sessions physically apart |
| 6 | Slash commands / Skills | Hands-on | Freeze repeated workflows into /cmd |
| 7 | Permission & safety model | Conceptual | What sits behind allow / deny / skip |
| 8 | Context management | Conceptual | /clear, /compact, dealing with context rot |
Hands-on sections come with copy-paste-able snippets. Conceptual sections explain “why this is shaped this way” and “when to reach for it”.
1. Hooks: scripts on lifecycle events
Hooks are the single most underrated capability in Claude Code. They slice the session into a set of lifecycle events and let you inject your own scripts at each point:
| Event | Fires when | What I do with it |
|---|---|---|
SessionStart | Session begins | Inject project state, git status, unread issues |
UserPromptSubmit | After enter, before model reads | Rewrite or reject the prompt (block secrets, inject project metadata) |
PreToolUse | Before a tool call | Intercept dangerous commands, block writes to certain paths |
PostToolUse | After a tool call | Auto-lint, auto-test, append to changelog |
Notification | Model is waiting for input | Toast / phone push |
Stop | Main turn ends | Wrap-up notification, log line |
SubagentStop | Subagent turn ends | Per-agent timing / signal |
Registration goes in ~/.claude/settings.json or the project’s .claude/settings.json. The two most useful patterns:
Pattern one: hang automation on either side of model actions. For example, run ESLint on every Edit and feed the errors back. Telling lint errors to the model directly works far better than “you forgot to lint, please remember next time” — the model fixes them on the next turn without being asked.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "node /Users/me/.claude/hooks/lint-changed.js",
"timeout": 30
}
]
}
]
}
}
matcher is a regex over tool names — Edit|Write triggers on file-write tools. The script reads the hook payload from stdin (including the path of the file that changed), runs lint, and prints errors to stdout. A non-zero exit code makes Claude Code feed stderr into the model’s next input.
Pattern two: wire session state to the outside world. I wrote a whole post on Mac notifications + Bark push
earlier — the meat of it is a Python script bound to Stop and Notification, throwing a local macOS toast and a remote push to my phone. Long jobs no longer require staring at the screen.
Paths must be absolute
A hook’s cwd is unpredictable (it depends on which project the session was launched from), and~/... doesn’t expand. Write everything as absolute paths.The real strength of hooks is that they turn “remind the model to do X” into “the system enforces X”. Instructional prompts get forgotten. Hooks don’t.
2. CLAUDE.md: a project manual for the agent
CLAUDE.md looks trivial — it’s just a Markdown file injected into every session’s system prompt. The leverage comes from layering:
~/.claude/CLAUDE.md # global: injected for all projects
<repo>/CLAUDE.md # project: only when cd'd into that repo
<repo>/<subdir>/CLAUDE.md # subdir: only for work in that subtree
Merge order is outside-in, with inner files taking precedence. Combine them like this:
- Global holds “who I am, what I prefer”. e.g. “I’m Jason, prefer functional style”, “use formal terms over slang in Chinese replies”.
- Project holds “the implicit rules of this repo”. e.g. “use pnpm not npm”, “must run
pnpm testbefore committing”, “Tailwind colors only fromtokens.ts, nobg-red-500”. - Subdir holds “the local conventions for this slice”. e.g.
apps/api/CLAUDE.mdsays “every endpoint needs a zod schema”.
What works best to write in there? A few rules I’ve internalized:
- Write the traps the model will fall into, not the obvious. Don’t write “we use React” — the model will read package.json. Do write “our
useFetchmust live outside the try/catch because internal handling already wraps”. - Write things that pay off forever once taught. Boris Cherny’s “compounding engineering” — each mistake the model makes, once captured, never has to be re-explained. Add the small ones: “yesterday it imported logger from the wrong path again”.
- Give a reason where you can. “Use X not Y” is weaker than “use X not Y because Y triggers ESM/CJS interop issues”. Reasons make the rule generalize.
- Keep it lean. CLAUDE.md loads every session and eats context. My personal red line is 200 lines per file. Beyond that, split into subdirs or move to slash commands that load on demand.
A trimmed sample of my project-level template:
# Project: lifelog
## Stack
- Hugo (extended) + custom theme `inkstone` (git submodule)
- Posts in content/posts/, bilingual: foo.md + foo.en.md
- Linked via translationKey
## Writing conventions
- First person, opinionated
- Open with a blockquote
- For long posts, give a map of the article in section 0
- Avoid colloquial translations of jargon
## Operational conventions
- Local preview: `hugo server -D`
- Before committing: `hugo --minify` to check for build errors
- Never `git push` directly. I'll do it.
That last line pairs with §7 below — agents that see this will pull back on their own.
3. MCP server: wrap external capabilities as tools
MCP (Model Context Protocol) is Anthropic’s open protocol that lets any external tool/data source plug into Claude (or any compatible client) through a uniform interface. Anthropic’s own line: MCP is to LLMs what USB-C is to hardware.
Claude Code ships with a few built in (filesystem, bash, web fetch), but the real unlock is custom MCP servers. The three primitives map to three “ways an agent knows things”:
| Primitive | Meaning | Analogy |
|---|---|---|
| Tools | Actions the model can invoke | Functions |
| Resources | Data the model can read | Files |
| Prompts | Templates the user can trigger | Shortcuts |
Fastest way to register a server with Claude Code is the CLI:
# Add a local stdio server
claude mcp add my-tools \
--command "node /Users/me/mcp-servers/my-tools/index.js"
# Add an HTTP server
claude mcp add notion \
--url "https://mcp.example.com/notion" \
--header "Authorization: Bearer xxx"
# See what's currently mounted
claude mcp list
Writing a minimal custom server isn’t hard either. Here’s a “read a Linear issue” skeleton in Node.js + the official SDK:
// index.js
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = new Server(
{ name: "linear-mini", version: "0.1.0" },
{ capabilities: { tools: {} } }
);
server.setRequestHandler("tools/list", async () => ({
tools: [
{
name: "get_issue",
description: "Fetch a Linear issue by identifier (e.g. ENG-123)",
inputSchema: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
},
],
}));
server.setRequestHandler("tools/call", async (req) => {
if (req.params.name !== "get_issue") throw new Error("unknown tool");
const { id } = req.params.arguments;
const r = await fetch(`https://api.linear.app/graphql`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: process.env.LINEAR_API_KEY,
},
body: JSON.stringify({
query: `query($id:String!){ issue(id:$id){ title description state { name } } }`,
variables: { id },
}),
});
const data = await r.json();
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
});
await server.connect(new StdioServerTransport());
Once mounted, just say “look at issue ENG-123” in the session and the model calls get_issue.
How I think about it:
- If a stock MCP exists, don’t write one. GitHub, Notion, Slack, Postgres all have official/community servers. Browse modelcontextprotocol.io first.
- Custom MCP fits “private / internal / cross-tool” capabilities. Internal status board, your personal notes vault, the NAS at home — that kind of thing.
- A failing MCP server doesn’t kill the session, it degrades. If a server can’t start at launch, the whole session still runs; that one tool is just unavailable. So mount freely.
4. Parallel agents: fan out subagents
Claude Code ships with a Task tool that fans out subagents from the main session in parallel. Not a gimmick — it’s the feature that genuinely changes work rhythm.
Typical fits:
- Large refactors: “rename X to Y” splits into “find all references”, “rewrite src/”, “rewrite test/”, “rewrite docs/”, four lanes in parallel
- Multi-stack comparison: two subagents implementing the same component in React and Vue, return for diffing
- Code review: one subagent for security, one for performance, one for readability
Triggering it from the main session is just natural language: “fan out 3 subagents to check type errors in src/api/, src/components/, src/hooks/ respectively, then summarize.” The model uses Task automatically.
To freeze it as a standard flow, define named subagents under .claude/agents/:
<!-- .claude/agents/security-reviewer.md -->
---
name: security-reviewer
description: Code review agent focused on security
tools: Read, Grep, Glob, Bash
---
You are a code reviewer focused on the OWASP Top 10.
For each finding, output:
- **Severity**: Critical / High / Medium / Low
- **Location**: file:line
- **Class**: SQL Injection / XSS / SSRF / ...
- **Fix**: a specific change
Security only. No style or performance feedback.
After this, “use the security-reviewer subagent to scan src/api/” lights it up directly. The tools: field locks down what the subagent is allowed to use (omit Edit/Write to keep it strictly read-only).
A handful of things I’ve learned the hard way:
- The practical ceiling is ~5–8 in parallel. Beyond that, token cost and context-switching cost dominate.
- Subagents don’t share the main session’s context. They only see their own prompt and the tools they can call. The instruction has to be self-contained.
- Merging the results is the main agent’s job. After getting N outputs back, the main agent still has to synthesize. This step quietly determines whether the parallelism actually paid off.
- Not every task is parallelizable. Strongly coupled tasks (B’s input is A’s output) get slower when forced into parallel. Test: can you write the work as several truly independent subtasks?
5. Worktree isolation: physically separate sessions
Another granularity of parallelism is multiple Claude Code main sessions on the same repo at once. The pitfall: they all touch the same working tree and stomp each other.
git worktree was made for this:
# Main repo at ~/code/myapp (working on main as usual)
cd ~/code/myapp
# Spawn two isolated working dirs on independent branches
git worktree add ../myapp-feat-a -b feat/a
git worktree add ../myapp-feat-b -b feat/b
# Three physical directories now exist:
# ~/code/myapp (main)
# ~/code/myapp-feat-a (feat/a)
# ~/code/myapp-feat-b (feat/b)
# Same .git, fully isolated working trees
Open three terminals, cd into each, and claude in each. Three agents working on three things, no cross-contamination.
My flow:
- For complex tasks, drop into plan mode (
shift+tab) in the main repo and split the work with the model into 3–5 orthogonal subtasks. - Each subtask gets a worktree (named
<repo>-<task-slug>). - Open three terminal panes (tmux / Warp split), one Claude per pane.
- After everything finishes, return to the main repo,
git worktree list, review each, merge,git worktree remove.
git worktree list # what's currently active
git worktree remove ../myapp-feat-a # tear down when done
git worktree prune # clean stale references
A few notes:
- Each worktree gets its own branch. Two worktrees on the same branch is rejected by git (and that’s a feature).
node_modules/.venvaren’t shared. Each worktree reinstalls. pnpm’s content-addressable store or a venv symlink can help, at the cost of more setup.- Cross-worktree env vars /
.envdon’t auto-copy.cpor symlink as needed. - A worktree is not a fork. All worktrees share the same
.git/, sogit fetchupdates everything once and commits land in the same object database. That’s why it’s much faster than a fullgit clone.
Worktrees and parallel subagents are two different parallels. Subagents fan out inside one session. Worktrees physically isolate between sessions. The former is fast but coupled, the latter is slower but cleaner. I reach for worktrees on refactors / cross-module changes, and for subagents on investigation / inspection / generation tasks.
6. Slash commands / Skills: freeze the workflow
/command is the other compounding investment in Claude Code. Anything you’ve done twice should be considered for a slash command on the third pass.
Where they live:
~/.claude/commands/<name>.md # global, available everywhere
<repo>/.claude/commands/<name>.md # project-scoped
The file itself is Markdown that gets injected as a prompt. Here’s the /review I use:
<!-- .claude/commands/review.md -->
---
description: Self-review the diff between this branch and main
allowed-tools: Bash(git diff:*), Bash(git log:*), Read
---
Self-review every change on this branch relative to main:
1. Run `git diff main...HEAD --stat` to see which files changed
2. Run `git diff main...HEAD` for the full content
3. Give feedback on:
- Abstractions that add complexity without value
- Violations of established conventions (consult CLAUDE.md)
- Test coverage (especially new public functions)
- Lingering console.log, debugger, TODO
Finish with a markdown review report I can paste straight into the PR.
Type /review in a session and the whole thing fires. The allowed-tools field temporarily whitelists git diff/log even if they aren’t in the global allowlist — safe by default.
Arguments are easy too — use $ARGUMENTS in the file:
<!-- .claude/commands/explain.md -->
Explain in detail what $ARGUMENTS does, why it's written this way,
and whether there's a simpler alternative.
In session, /explain src/utils/debounce.ts substitutes the path in.
My current resident commands:
| Command | Purpose |
|---|---|
/review | Self-review the current branch |
/plan | Decompose a request into a TODO list, wait for approval before acting |
/explain <path> | Explain a piece of code |
/tidy | Run lint + format + fix obvious type errors |
/post | Blog-only — scaffold front matter + outline for a new post |
Skills (the broader “packaged capability” idea) are still evolving, but the principle is the same: turn “things you keep re-explaining to Claude” into “one-line triggers that are now project assets”. Each one is permanent dividend.
7. Permission & safety model: allow / deny / skip
The permission system is one of the things that sets Claude Code apart from generic “AI editors”. Three tiers:
| Mode | Behavior | When |
|---|---|---|
| Default (ask) | Every tool call prompts | New projects, first time using an MCP |
| Auto-accept | Tools on the allowlist auto-pass | Daily |
| Bypass / dangerouslySkipPermissions | Everything passes | Sandbox runs, throwaway scripts |
The permissions block in settings.json is where you tune most often:
{
"permissions": {
"allow": [
"Bash(npm test:*)",
"Bash(pnpm:*)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)",
"Edit",
"Write",
"Read"
],
"deny": [
"Bash(rm -rf:*)",
"Bash(chmod:*)",
"Bash(curl:*)",
"Bash(git push:*)",
"Bash(git commit:*)"
]
}
}
deny beats allow. My personal style is conservative:
- Deny anything genuinely dangerous you can name.
rm -rf,git push,git commit,curl(defends against exfil),chmod,sudo. - Allow common reads.
git status,git log,ls,pwd,cat, the test commands. - Don’t allowlist writes. I leave
Edit/Writeon default ask — the extra prompt feels slow but the floor is solid.
--dangerously-skip-permissions (a launch flag) is the nuclear option: every check is bypassed. It is not for daily use. Reserve it for:
- Docker / VM sandboxes where blowing things up is “rebuild and move on”
- One-shot script tasks, e.g. batch-renaming 1000 files while you’re literally watching
- CI / Headless runs where the environment is throwaway by design
Never flip on dangerouslySkipPermissions on your daily-driver setup. The day a hallucination decides to rm -rf ~, you’ll cry.
A safer alternative is sandboxing: run Claude in a devcontainer / Docker / Multipass VM, physically isolated. Anthropic’s own security best practices recommend this.
The point of the permission model is to let agents act autonomously inside a defined zone, and force a stop at the edge. Allow too loose and you eat damage; deny too aggressive and you’re back to “co-pilot mode” pressing accept on everything, losing the agentic value. The dial is per-project and per-personality.
8. Context management: context is king, but it rots
Last one, conceptual. Karpathy’s much-quoted line:
“Context engineering is the delicate art and science of filling the context window with just the right information for the next step.”
And Anthropic’s own research keeps confirming a counterintuitive point: more context isn’t better. Models exhibit “context rot” under long inputs — even on simple tasks, performance degrades as input length grows.
Claude Code gives you a few knives:
| Command | What | When |
|---|---|---|
/clear | Wipe context, start fresh | Switch to a totally unrelated task |
/compact | Have the model self-summarize the current chat | Same task, but too much accumulated chatter |
/resume | Recover detail that was compacted away | Occasional need to look back |
How I use them:
- Task boundaries are the natural
/clearpoint. Done with feature A and now fixing bug B?/clearfirst. - 30+ turns deep on the same task and the model starts drifting?
/compact. It re-focuses noticeably. - CLAUDE.md is “permanent context”, don’t let it rot into a junk drawer. Every line should earn its keep.
- Long reference material doesn’t go in chat — write it to a file and let the model
Readit.Readis on-demand; pasting is once-and-stuck.
A deeper point: subagents are a context-management tool. Each fanned-out subagent gets a clean window; only the summary returns to the main. That’s why a big refactor is faster as fan-out than as one main agent grinding through — not because parallelism saves wall time, but because each sub-context stays cleaner.
Like the permission model, context management is a question of timing. The only rule: when the model starts repeating, looping, or forgetting, act. That intuition only develops with reps.
9. Closing: from tool to team
Stepping back from eight sections, the through-line is:
What’s most valuable about Claude Code isn’t that it can write code. It’s that it can be configured into a part of how you work.
Hooks are reflex arcs. CLAUDE.md is long-term memory. MCP is sense extension. Subagents are clones. Worktrees are physical isolation. Slash commands are muscle memory. The permission model is the boundary. Context management is restraint. Composed together, what you’re managing isn’t “an AI editor” anymore — it’s a small team with reflexes, memory, parallelism, boundaries, and the discipline to rest.
If you can only adopt three:
- Every project gets a CLAUDE.md under 200 lines. Cheapest compounding move there is.
- Anything you’ve done twice, on the third pass becomes a slash command or a hook.
- Tasks longer than 30 minutes go through plan mode; complex changes go straight into a worktree.
The rest — MCP, subagents, custom servers — is “next step”. Add when needed, skip when not. The value of a tool is what it does when you’re not there, not how many of them you collected.
References:
- Claude Code official docs
- MCP protocol homepage
- Anthropic Engineering: Claude Code Best Practices
- How Anthropic Teams Use Claude Code
- My earlier post: Wiring Claude Code into macOS Notifications + Bark Push
- My earlier post: CS146S course notes (Week 3 / Week 4 overlap most with this one)