Agent workbench (capstone)
Compose Claude Code config, a custom Skill, a subagent, and your own MCP server into one automated, verifiable developer workbench — the whole section in one repo, shipped to the team.
Learning objectives
- Assemble config, a Skill, a subagent, and an MCP server in one repo.
- Automate quality with a hook and CI.
- Apply the 4 Ds across the setup.
- Ship it so the team inherits the workbench.
code/proj-ak-workbench/. Python runs offline; configs are ready to use.1 · The repo layout essential
You've felt this failure even if you've never named it. You spent an afternoon teaching Claude your project's quirks — the test command, the module you never touch, the review style you like — and it was great. Then a teammate opened the same repo, got a Claude that knew none of it, and shipped a change your way of working would have caught. All that steering lived on your laptop, in your head and your loose config, and it evaporated at the edge of your machine. A workbench fixes exactly that: it makes the steering part of the repo.
Think of the repo layout like the shared kit in a professional kitchen. It isn't the food — it's the mise en place: knives sharpened, stations labelled, the recipe card pinned where everyone reads it. Any cook who walks in inherits the setup and works to the same standard on day one. That is what the .claude/ folder is: a station every teammate's Claude sits down at, pre-configured. CLAUDE.md is the recipe card (conventions and commands), settings.json is the safety guard on the equipment (permissions and hooks), and skills/, agents/, and mcp/ are the specialist tools bolted to the bench.
The precise idea worth holding onto: everything that shapes Claude's behaviour lives in version-controlled files, not in a person. Why does that separation earn its own opening section? Because it flips configuration from a private habit into a shared, reviewable, git-tracked artifact — the same shift that turned "works on my machine" scripts into real infrastructure. The common mistake is treating .claude/ as personal scratch and .gitignore-ing it; do that and you're back to steering that dies on one laptop. Commit it, and one git pull hands the whole team your judgment.
git pull hands every teammate the same steering, limits, Skills, and tools.
Read top-to-bottom in two hops: the repo at the top branches down (blue arrows) into the files that configure Claude, and those files then converge (dashed arrows) into the green payoff at the bottom.
- The top box is your committed repo. The four coloured boxes below it are the pieces that shape Claude's behaviour —
CLAUDE.md(conventions),settings.json(permissions + a hook), theskills/andagents/folders (a Skill and a subagent), andmcp/plus the CI workflow. The K-tags point back to the lesson that introduced each. - The solid blue arrows fan out from the repo: every one of these lives inside version control, not on a person's machine.
- The dashed arrows fan in to the green box — the payoff. Because all of it is committed, one
git pullgives a teammate the whole workbench at once: your steering, limits, Skills, and tools, with no setup call.
In short: steering that lives in files travels with the repo; steering that lives in your head dies at the edge of your laptop.
tree.txtyour-project/
├── CLAUDE.md # K1: conventions, commands, guardrails
├── .claude/
│ ├── settings.json # K1/K2: permissions + a Stop hook (tests)
│ ├── commands/review.md # K2: /review slash command
│ ├── skills/house-style/SKILL.md # K4: team code + doc conventions
│ └── agents/code-reviewer.md # K5: least-privilege reviewer subagent
├── mcp/server.py # K6: MCP server exposing project tools/data
└── .github/workflows/claude-review.yml # K2: headless review on PRs
settings.json is where you tell Claude Code the rules of the house. It's plain JSON — a set of "key": value pairs wrapped in curly braces. Two sections matter here: permissions (what's allowed) and hooks (what runs automatically).
"allow"lists actions Claude may take without asking — runpytestorruff, read any file (Read(*)), and edit files undersrc/. The patterns in parentheses scope each permission narrowly."deny"is the safety rail: it blocksgit pushand any edit tovendor/. Deny always wins over allow, so risky actions stay in human hands.- The
"Stop"hook fires when Claude finishes a turn."matcher": "*"means "on every stop", and it runs one command. - That command,
pytest -q || echo TESTS-FAILED, runs your tests quietly; the||means "if pytest fails, print TESTS-FAILED" so a failure is loud and visible instead of silent.
What the output means: No visible output on its own — this file just configures Claude Code. Its effect is felt later: blocked pushes, and tests running automatically each time Claude stops.
Try this: Add "Bash(git commit:*)" to the allow list and think about whether you want that. The allow/deny split is exactly where you decide what Claude does on its own versus what needs you.
Before any code, this is a map of the repo you're about to build. Everything Claude reads to behave your way lives in one hidden folder, .claude/, next to your normal project files. A newcomer only needs to know: each file below is one capability, and the capital-K tags (K1, K2, …) point back to the lesson that introduced it.
CLAUDE.mdsits at the top — plain-English notes (conventions, common commands, guardrails) that Claude reads automatically every session, like an onboarding doc..claude/settings.jsonholds permissions (what Claude may or may not do) and hooks (commands that fire automatically at certain moments).commands/,skills/, andagents/each add one thing: a reusable/reviewshortcut, a Skill (packaged know-how), and a subagent (a focused helper with its own narrow permissions).mcp/server.pyis your own small server that hands Claude project data and actions;.github/workflows/…ymlruns a review automatically on every pull request.
What the output means: This is a directory tree, not a program — nothing runs. The ├── and └── lines just show which files live inside which folders (indentation = nesting).
Try this: Picture cloning this repo fresh: the moment you open it, Claude already knows your rules (CLAUDE.md), your limits (settings.json), and your tools. That inheritance-from-one-git pull is the whole point of the workbench.
2 · Wire the config essential
The scariest bug an agent can hit isn't a wrong answer — it's a right-looking action taken without asking. An agent that "helpfully" runs git push to a shared branch, or edits a vendored library you'd sworn never to touch, has done something no test will catch until it's already out. The config file is where you decide, up front and in writing, which actions Claude may take alone and which must stop for a human. Get it wrong and you either babysit every step (useless) or hand over the keys to the whole car (dangerous).
The mental model is a building's access-card system. Some doors open for anyone with a badge (the break room), some need a manager's card (the server room), and a few are welded shut no matter whose card you wave (the roof). Claude Code's settings.json works the same way: an allow list is the doors that open freely, the deny list is the welded doors — and crucially, deny always beats allow. If an action matches both, deny wins. That ordering is the whole safety story: a broad allow pattern can never accidentally re-open something you explicitly closed.
Two things go in this one file, and it's worth seeing why they belong together. Permissions answer "what may Claude do?"; hooks answer "what happens automatically at certain moments?" — like a Stop hook that runs your tests every time Claude finishes a turn. The common mistake is to lean on the allow list alone, quietly omitting the dangerous stuff and hoping it never comes up. Omission isn't protection: an unlisted action falls through to "ask," which is fine until a broad pattern you added later silently covers it. Spell the dangerous actions out on the deny list so the guarantee is explicit, not accidental.
git push, vendor/) can never be re-opened by a broad allow — and anything matching neither falls through to ASK.
Follow one action left to right along the top row; the two downward arrows are the two ways the search can stop with a decision.
- The blue box on the left is the incoming action (a tool plus its argument, e.g.
Bash(git push …)). The solid arrow carries it into the red "deny list?" gate — the very first check. - If it matches the deny list, the downward arrow lands on DENY and the search is over — this is why deny beats allow. A dashed "no" arrow means it wasn't denied, so it steps right to the green "allow list?" gate; a match there drops to ALLOW.
- If it matches neither, the last dashed arrow falls through to the amber ASK box — the safe default that puts the decision back in a human's hands.
In short: deny first, then allow, then ask — so the dangerous doors you welded shut stay shut no matter how broad an allow you add later.
settings.json{
"permissions": {
"allow": ["Bash(pytest:*)", "Bash(ruff:*)", "Read(*)", "Edit(src/**)"],
"deny": ["Bash(git push:*)", "Edit(vendor/**)"]
},
"hooks": {
"Stop": [ { "matcher": "*", "hooks": [
{ "type": "command", "command": "pytest -q || echo TESTS-FAILED" } ]} ]
}
}
3 · The MCP server (K6) intermediate
Here's the ceiling you hit with a well-configured Claude that has no tools of its own. You ask "which tickets are open on the billing component?" and it can't answer — it has no way to reach your tracker. So you copy-paste the ticket list into the chat, and the deploy log, and yesterday's incident, every single time. The intelligence is there; the reach isn't. An MCP server is how you give Claude arms long enough to touch your own systems.
Think of MCP (Model Context Protocol) as a universal wall socket. Before standard sockets, every appliance needed its own custom wiring into the mains; after, anything with the right plug just works. MCP is that standard plug for tools: you wrap your ticket tracker or deploy log behind a small server that speaks the protocol, and any MCP-aware host — Claude Code here — can plug in without bespoke integration. FastMCP is the convenience layer that turns an ordinary, typed Python function into one of those pluggable tools with a single @mcp.tool() decorator.
The subtle, load-bearing point — the one that separates a working server from a useless one — is that the docstring is the interface, not documentation. Claude doesn't run your tools by reading their code; it reads each tool's docstring and type hints to decide which one to call. A vague description ("gets stuff") means the model routes to the wrong tool or none at all; a precise one ("List open tickets for a component; use when triaging or planning") routes reliably. So write docstrings as if they were the tool's public spec — because to the model, they are. And keep the tool surface small: every tool you expose is one more thing the model must choose correctly between, so expose only what the workbench actually needs.
open_tickets) gets called reliably while a vague one is skipped or mis-picked — write the docstring as the tool's public spec.
Trace the request left to right: a question enters, Claude inspects the available tools, and the arrow that fires depends entirely on how the tool describes itself.
- The blue box on the left is a user request. The solid arrow carries it to the middle box, where Claude reads the docstring and type hints of every registered tool — not their implementation.
- The solid "match" arrow to the green box is the win:
open_tickets()has a precise docstring ("List open tickets…"), so the model routes there confidently and calls it. - The dashed "no match" arrow to the red box is the failure mode: a tool whose docstring is vague gets skipped or picked by mistake. That is why you write docstrings as a spec and keep the tool surface small — every extra tool is one more choice the model can get wrong.
In short: to the model the docstring is the tool; clear descriptions route reliably, vague ones route wrong.
server.pyfrom mcp.server.fastmcp import FastMCP
mcp = FastMCP("project-tools")
def query_tracker(**kw): ... # your ticket-system integration
def read_deploy_log(service): ... # your deploy-log integration
@mcp.tool()
def open_tickets(component: str) -> list[dict]:
"""List open tickets for a component. Use when triaging or planning."""
return query_tracker(status="open", component=component)
@mcp.tool()
def last_deploy(service: str) -> dict:
"""Return the last deploy's status and commit for a service."""
return read_deploy_log(service)
if __name__ == "__main__":
mcp.run(transport="stdio")
An MCP server is a small program that exposes your own tools to Claude in a standard way (MCP = Model Context Protocol). Here it's built with FastMCP, a helper that turns ordinary Python functions into tools Claude can call. Once this runs, Claude can look up tickets and deploy status through your systems.
mcp = FastMCP("project-tools")creates the server and names it. That name is how Claude will refer to this bundle of tools.query_trackerandread_deploy_logare placeholders (the...means "you fill this in") — they stand for wherever your real ticket system and deploy logs live.- The
@mcp.tool()line above a function is a decorator: it registers that function as a callable tool. The docstring right under it ("""List open tickets…""") is not a comment — Claude reads it to decide when to use the tool, so it must describe the purpose plainly. - The
-> list[dict]and-> dicttype hints tell Claude the shape of the answer. Finallymcp.run(transport="stdio")starts the server, talking over standard input/output so Claude Code can connect to it locally.
What the output means: Run directly, this prints nothing and waits — an MCP server is a background service, not a script that finishes. Claude Code starts it and calls its tools on demand.
Try this: Read each tool's docstring as if you were Claude deciding which to call. A vague docstring means the wrong tool gets picked — clear descriptions are how the model routes to the right tool.
4 · Advanced — Skill + subagent advanced
You've probably repeated yourself into exhaustion. Every session you retype the same reminders — "use type hints, lead docs with the why, never except: pass" — and every session Claude forgets them the moment the window closes. Worse, when you hand a big review to your one general assistant, it can wander: it starts editing the file it was only meant to critique. Skills and subagents are the two fixes for those two pains — and it's worth being clear which solves which, because beginners conflate them.
A Skill is packaged know-how; a subagent is a scoped worker. The analogy that keeps them straight: a Skill is the laminated procedure card you pin above a workstation ("here's how we do it here"), while a subagent is a specialist you hire with a deliberately limited badge. The procedure card is pure instruction — text Claude loads when its description says the situation applies. The specialist is a separate persona with its own tool permissions — a code reviewer you give read-only access, so it physically cannot edit or run anything even if it wanted to.
Why draw the line so sharply? Because the Skill answers "how should this be done?" and the subagent answers "who does it, and what may they touch?" — and conflating them is the classic mistake. People stuff behaviour rules into a subagent that every session needs (should have been a Skill, always available) or, worse, give a reviewer subagent broad edit rights "just in case" (defeating the entire point of a scoped worker). A reviewer that can write files is a liability the day a prompt injection or a confused turn tells it to; narrowing it to read-only shrinks the blast radius to nothing. Least privilege isn't paranoia here — it's the feature.
Read the two panels side by side — left is the Skill, right is the subagent — and compare the bottom row of each, which is where they truly differ.
- The purple panel (left) is the Skill: pure instruction. Its top strip is the
descriptionthat decides when it loads; the strip below is the actual house rules. It never has permissions of its own — it's text. - The green panel (right) is the subagent: a worker with an identity. Its top strip is its own tool permissions; the red strip at the bottom is the load-bearing part — Read-only, no Edit, no Bash.
- That red strip is the payoff: because the reviewer physically lacks write and shell tools, a prompt injection or confused turn can't make it change anything. The Skill governs how; the subagent governs who and what-they-may-touch.
In short: behaviour rules everyone needs → a Skill; a focused job with a deliberately small badge → a subagent scoped to least privilege.
house-style.md---
name: house-style
description: >
Apply our team's code and doc conventions. Use whenever writing or editing
code, docs, or PR descriptions in this repo.
---
# House style
- Functions: type hints + a one-line docstring stating the contract.
- Errors: raise specific exceptions; never `except: pass`.
- Docs: lead with the "why", then the "how"; examples must be runnable.
- PRs: describe the change, the risk, and how it was verified.
A Skill is packaged know-how Claude loads on demand. The file has two parts: a front-matter header (between the --- lines) that tells Claude when to use the Skill, and a Markdown body that says what to do. This one encodes your team's coding and writing conventions.
- The
---fences mark YAML front-matter — structured metadata.nameis the Skill's id. descriptionis the crucial line: Claude reads it to decide whether this Skill applies. "Use whenever writing or editing code, docs, or PR descriptions" is a clear trigger, so the Skill activates at the right moments.- Everything below the closing
---is plain Markdown — the actual rules. Bullets keep them scannable: type hints, specific exceptions (neverexcept: pass), docs that lead with the "why". - Because it's just text, the Skill is version-controlled with the repo, so the whole team's Claude follows the same conventions automatically.
What the output means: No output — a Skill is instructions, not a program. Its effect shows up in the code and docs Claude writes: they start matching these house rules.
Try this: Add one rule you care about (e.g. "tests live next to the code they test"). The next time Claude edits this repo, that rule is in force — you've taught the whole team's assistant with one line.
5 · Professional — automate the review professional
A safeguard that depends on someone remembering to use it isn't a safeguard. You built a sharp code-reviewer subagent in §4 — but if running it is optional, it runs on the PRs you're not rushed on and skips the ones that matter most, the Friday-evening hotfix nobody double-checks. Any quality bar that lives only in a person's discipline erodes under deadline pressure. The move that makes it real is to take that same reviewer out of your hands and wire it into the pipeline, so it fires on every pull request whether anyone remembers or not.
The key realisation is that Claude Code has two doors, and this section walks through the second one. Interactively, you chat with it in your editor; headlessly, you invoke it as a plain command — claude -p "…" — that takes a prompt, does the work, and prints a result, no chat window involved. That headless door is what lets a machine, not a human, drive the agent. A GitHub Actions workflow is just a recipe that machine follows: on a pull request, check out the code, compute the diff, and pipe it through claude -p pointed at your reviewer subagent.
The leverage worth naming — and the reason this rung is "professional" — is write once, run in both doors: the exact subagent that helps you at your desk now guards every teammate's PR automatically, with zero extra code. That's the same encoded judgment reused, not a second implementation to maintain. The mistake to avoid is the tempting shortcut of pasting your API key straight into the YAML; that file is committed and shared, so the key leaks the moment the repo does. Read it from CI secrets instead (${{ secrets.… }}) — the automation stays safe to commit precisely because the credential never appears in it.
claude -p run of your reviewer subagent → review.md, with the key injected from CI secrets so the committed workflow never carries the credential.
Read the top row left to right — it's the pipeline in run order — then note the single arrow coming up from below.
- The blue box on the left is the trigger: opening a pull request starts the workflow. Each solid arrow is one step handing off to the next — checkout (with full history), then git diff against
mainto get just the change. - The purple box is the heart: a headless
claude -prun — no chat window — pointed at the very same reviewer subagent you use interactively. Its output lands in the greenreview.mdbox, which is printed into the PR logs. - The dashed arrow from the amber box shows where the credential enters:
ANTHROPIC_API_KEYis read from CI secrets at run time, so it never appears in the committed YAML.
In short: write the reviewer once, run it in both doors — your editor and every PR — with the key kept out of the file.
claude-review.ymlname: Claude review
on: [pull_request]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- env: { ANTHROPIC_API_KEY: "${{ secrets.ANTHROPIC_API_KEY }}" }
run: |
git diff origin/main...HEAD > /tmp/diff.txt
claude -p "Use the code-reviewer subagent on /tmp/diff.txt. \
Output findings grouped by severity." > review.md
cat review.md
This is a GitHub Actions workflow — a YAML recipe GitHub runs automatically. Its job: every time someone opens a pull request, spin up a fresh machine, run Claude in headless mode (no chat window) to review the change, and save the findings. This is your review, automated for the whole team.
on: [pull_request]is the trigger — the workflow fires on every PR. Theruns-on: ubuntu-latestline gives it a clean Linux machine to work on.actions/checkout@v4withfetch-depth: 0downloads the full git history so the diff can be computed againstmain.ANTHROPIC_API_KEYis read from repo secrets (${{ secrets.… }}) — the key is never written in the file, so it stays private.- The
run:block is shell: save the PR's changes to/tmp/diff.txt, thenclaude -p "…"runs one prompt non-interactively, asking the code-reviewer subagent to review that diff and group findings by severity, writing the result toreview.md.
What the output means: On each PR, a review.md is produced with the subagent's findings grouped by severity, and cat review.md prints it into the CI logs for anyone to read.
Try this: Notice the same subagent used interactively (K5) is reused here headlessly. Write once, run it both in your editor and in CI — that's the leverage of committing your setup.
6 · Professional — the 4 Ds, made concrete professional
Frameworks feel hollow until you can point at where they live in the code. "AI Fluency" and its four Ds — Delegation, Description, Discernment, Diligence — are easy to nod along to and just as easy to forget, because at the slide level they're abstractions. The failure mode is building a workbench that feels complete but has a silent gap: great steering and no verification, or tight tools and no way to keep dangerous actions human. The four Ds are the checklist that catches those gaps.
The reason this section exists is to close the loop between principle and artifact: every piece you've built maps to exactly one D, and seeing the map is what turns four vocabulary words into a design review you can actually run. Delegation is deciding what Claude does alone versus what stops for you — that's your permissions and deny-list. Description is carrying intent so the model knows what "good" means here — CLAUDE.md and the Skill. Discernment is verifying the work rather than trusting it — the Stop hook and CI review. Diligence is staying responsible over time — no-auto-push, secrets kept out of files, checks that gate the build.
Hold onto the payoff: if you can name which D a component serves, you can spot the one that's missing. A workbench strong on Description but weak on Discernment writes beautiful code nobody checks; strong on Delegation but weak on Diligence is safe today and leaky the day someone widens an allow rule. The common mistake is to treat the four as a feel-good summary at the end; instead use them as a gap-finder — walk each D, point at the file that satisfies it, and if you can't point at one, that's your next commit.
Read it in columns, top to bottom: the top row names the four Ds; the arrow under each drops to the exact workbench file (bottom row) that satisfies it.
- The top four boxes are the principles — Delegation (what Claude does alone), Description (carrying intent), Discernment (verifying, not trusting), Diligence (staying responsible over time).
- Each blue arrow maps a principle to the concrete piece beneath it: permissions/deny-list and the scoped subagent (Delegation), CLAUDE.md and the Skill (Description), the Stop hook and CI review (Discernment), no-auto-push and secrets-out-of-files (Diligence).
- The payoff is using the columns as a gap-finder: if any D has no file under it, that empty slot is the missing safeguard — and your next commit.
In short: name the D, point at its file; a column with nothing beneath it is the hole in your workbench.
The workbench is where AI Fluency becomes concrete: Delegation — the deny-list keeps risky actions human; Description — CLAUDE.md + the Skill carry intent; Discernment — the Stop hook + CI review verify; Diligence — permissions + no-auto-push keep it responsible.
workbench_check.pydef workbench_ready(repo):
required = {
"claude_md": "CLAUDE.md (K1)",
"settings_permissions": "permissions (K1)",
"stop_hook": "test-on-stop hook (K2)",
"skill": "a house Skill (K4)",
"subagent": "a subagent (K5)",
"mcp_server": "an MCP server (K6)",
"ci_review": "CI review (K2)",
}
missing = [desc for key, desc in required.items() if not repo.get(key)]
return (not missing), missing
full = dict(claude_md=1, settings_permissions=1, stop_hook=1, skill=1,
subagent=1, mcp_server=1, ci_review=1)
print("ready:", workbench_ready(full))
partial = dict(claude_md=1, settings_permissions=1, skill=1)
print("partial:", workbench_ready(partial))
ready: (True, [])
partial: (False, ['test-on-stop hook (K2)', 'a subagent (K5)', 'an MCP server (K6)', 'CI review (K2)'])
This little program checks that your workbench is complete — that every piece from the section (CLAUDE.md, permissions, the Stop hook, a Skill, a subagent, an MCP server, CI review) is actually present. It's a self-test you can run to prove the setup is whole, and unlike the configs above, it really runs.
requiredis a dictionary mapping a short key (like"stop_hook") to a human-readable label. It's the checklist of everything the workbench must have.repois expected to be a dictionary describing what's present.repo.get(key)returns the value for that key, orNoneif it's absent — sonot repo.get(key)is true for anything missing.- The line
missing = [desc for key, desc in required.items() if not repo.get(key)]is a list comprehension: it walks the checklist and collects the labels of every piece the repo lacks. return (not missing), missinghands back two things: a True/False "is it ready?" (not missingis True only when the list is empty) and the list of what's still missing. The two calls below test a full repo and a partial one.
What the output means: ready: (True, []) — the full repo passes with nothing missing. partial: (False, [...]) — the partial repo fails and names exactly the four pieces it lacks (the hook, subagent, MCP server, and CI review).
Try this: Delete one key from the full dict and re-run: that piece's label jumps into the missing list and ready flips to False. This is how you'd gate a 'workbench complete' check in CI.
7 · Tech-lead — ship the workbench to the team tech-lead
Picture the team you're actually on: ten engineers, ten laptops, ten slightly different Claudes. One person has a great deny-list, another has none; one wrote a lovely reviewer subagent nobody else knows exists. Everyone re-solves the same setup, badly and inconsistently, and the org's collective judgment about "how we work with Claude here" lives nowhere. That inconsistency is the problem this final rung solves — and it's a leadership problem, not a coding one.
The shift in thinking is from personal tool to institutional asset. A great individual setup helps one person; a committed setup helps everyone who ever clones the repo, including the teammate who joins next year. This is the same leap that made linters and CI worth adopting: value that scales with the team because it's enforced by the system, not by each person's memory. When the whole .claude/ tree and the MCP server are in version control, a single git pull hands a newcomer your steering, your limits, your Skills, your tools, and your safety rails — day-one parity, no onboarding call.
The precise payoff is what the course calls institutional leverage: your judgment, encoded once, enforced automatically for every engineer's Claude. The mistake that quietly kills this is treating the config as personal and leaving it out of the commit — then the leverage evaporates and you're back to ten laptops. The tech-lead move is the opposite instinct: whenever you improve how your own Claude behaves, ask "should the whole team have this?" and if yes, commit it. That one habit is how a good engineer's taste becomes the team's baseline.
git pull, so they all inherit the same steering, Skills, subagent, and safety rails.
Compare the two halves across the dashed line: the left is today's mess, the right is what committing the workbench buys you.
- The red boxes on the left are ten engineers each configuring Claude on their own laptop — one has a good deny-list, another has none, a third wrote a Skill nobody else knows about. Nothing is shared, so the setups drift apart.
- The purple box on the right is the single committed
.claude/tree plus the MCP server, living in one repo. The bluegit pullarrows fan out to every engineer. - The green boxes are the payoff: because it all came from one repo, every teammate's Claude is identical — same steering, limits, Skills, subagent, and tools, with no onboarding call. That uniformity is the institutional leverage.
In short: config on laptops diverges; config in the repo converges — one git pull turns your judgment into the team's baseline.
Commit the whole .claude/ tree + the MCP server. Every teammate who clones the repo inherits your steering, Skills, subagents, and safety — the entire section, working for the whole team from one git pull.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: A shared agent workbench is only as safe as its permission model. The foundation is a .claude/settings.json where a deny always beats an allow, so a risky action stays human even under a broad allow.
Your task: Model the allow/deny permission resolution: given an action, decide ALLOW, DENY, or ASK, with deny checked first and unmatched actions defaulting to a human prompt.
Requirements:
- Parse rules of the form
Tool(pattern:*)into a tool name and argument pattern - Match an incoming action (tool + argument) against a rule
- Check the deny list first — a deny wins even if an allow also matches
- Fall through to ASK when neither list matches
- Show a safe command allowed, a risky command denied, and an unknown command asking
💡 Hint: Order is the whole lesson: evaluate deny before allow so a broad allow can never re-open something the deny list closed.
Show solution
The permission resolver — deny wins (pure stdlib, mirrors settings.json semantics):
ALLOW = ["Bash(pytest:*)", "Bash(ruff:*)", "Read(*)", "Edit(src/**)"]
DENY = ["Bash(git push:*)", "Edit(vendor/**)"]
def matches(rule, action):
tool, pat = rule[:-1].split("(", 1) # e.g. Bash(pytest:*)
return action["tool"] == tool and action["arg"].startswith(pat.rstrip("*"))
def decide(action):
if any(matches(r, action) for r in DENY): # deny checked first, wins
return "DENY"
if any(matches(r, action) for r in ALLOW):
return "ALLOW"
return "ASK" # default: prompt the human
print(decide({"tool": "Bash", "arg": "pytest -q"})) # ALLOW
print(decide({"tool": "Bash", "arg": "git push origin"})) # DENY
print(decide({"tool": "Bash", "arg": "rm -rf /"})) # ASK
The allow-list grants specific safe actions; the deny-list keeps risky ones (git push, vendor edits) human — and deny beats allow so a broad allow can't accidentally open a dangerous door. Anything unmatched falls through to ASK, the safe default.
Context: Quality you rely on a human to remember isn't automated. A Stop hook makes the harness run the tests every time Claude finishes, and a CLAUDE.md makes team conventions travel with the repo instead of in one person's head.
Your task: Define a .claude/settings.json Stop hook that runs the test suite on every finish and a CLAUDE.md carrying conventions, then reason about what the hook guarantees.
Requirements:
- A Stop hook under the
hookskey with a matcher that fires on every stop - A command-type hook that runs the tests and surfaces failure loudly
- A CLAUDE.md holding conventions and common commands in plain Markdown
- Explain that the harness — not the model — runs the hook, so it can't be skipped
- State the guarantee: no change quietly completes with failing tests
💡 Hint: Use a * matcher and a command like pytest -q || echo TESTS-FAILED so a failure is visible; the reliability comes from the harness executing it, not the model.
Show solution
The Stop hook as literal config (this is .claude/settings.json, not runnable Python):
{
"permissions": {
"allow": ["Bash(pytest:*)", "Read(*)", "Edit(src/**)"],
"deny": ["Bash(git push:*)"]
},
"hooks": {
"Stop": [
{
"matcher": "*",
"hooks": [
{ "type": "command", "command": "pytest -q || echo TESTS-FAILED" }
]
}
]
}
}
The Stop hook runs pytest automatically whenever Claude stops, so no change is "done" until the suite is green — the harness executes this, not the model, so it cannot be skipped. CLAUDE.md carries the conventions and commands so intent travels with the repo, not in one person's head.
Context: Claude is far more useful when it can reach your systems. An MCP server exposes your ticket tracker and deploy logs as standard tools, with docstrings acting as the routing hints the model reads to pick the right one.
Your task: Show the correct FastMCP server shape: typed tool functions with docstrings that guide routing, run over stdio so Claude Code can spawn it. Mark this rung as needing the SDK.
Requirements:
- Create a FastMCP server with a descriptive name
- Register each tool with the tool decorator on a typed function
- Write a clear docstring per tool — the model reads it as a routing hint
- Give return type hints so the model knows the response shape
- Run over stdio so Claude Code can launch it as a subprocess
- Label the rung as requiring the MCP SDK
💡 Hint: The docstring is the interface the model routes on — treat it as a spec, not a comment — and return types tell the model what shape comes back.
Show solution
The MCP server — needs pip install mcp (documented FastMCP API):
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("project-tools")
def query_tracker(**kw): ... # your real integrations
def read_deploy_log(service): ...
@mcp.tool()
def open_tickets(component: str) -> list[dict]:
"""List open tickets for a component. Use when triaging or planning work."""
return query_tracker(status="open", component=component)
@mcp.tool()
def last_deploy(service: str) -> dict:
"""Return the last deploy's status and commit for a service."""
return read_deploy_log(service)
if __name__ == "__main__":
mcp.run(transport="stdio") # host launches this as a subprocess
Each @mcp.tool() registers a typed function whose docstring is the routing hint Claude reads to decide when to call it. Written once and committed, the server lets any teammate's Claude query tickets and deploys through the same standard tools — write-once, reuse-everywhere.
Context: A focused subagent does one job with exactly the access that job needs. A code reviewer that can edit files or run shell commands is a liability; narrowing it to read-only is the entire point.
Your task: Define a code-reviewer subagent spec that is read-only on files and outputs findings grouped by severity, then reason about why narrowing its permissions is the design goal.
Requirements:
- A Markdown subagent spec with a clear name and a single purpose
- Behaviour: analyse the diff for correctness and group findings by severity
- Tooling restricted to read-only — no edits, no shell
- An explicit, actionable output format (findings grouped critical/warning/note)
- Explain least-privilege: one job, minimum access, smaller blast radius
💡 Hint: Ask what a reviewer must NOT be able to do — the answer (write files, run commands) is exactly what you leave out of its tool list.
Show solution
The subagent definition (this is .claude/agents/code-reviewer.md — Markdown spec, not code):
# Code Reviewer
A focused subagent for reviewing diffs. Least-privilege: read-only on files.
Behavior:
- Analyze the diff strictly for correctness.
- Group findings by severity: critical (must fix), warning (should fix),
note (nice-to-have).
- Output markdown with line-by-line references.
Tools: Read only. No Edit, no Bash. It reviews; it does not change code.
A subagent is a narrow specialist with its own least-privilege permissions — read-only here, so a reviewer can never edit or run anything. Scoping it tightly is the safety point: it does exactly one job with exactly the access that job needs, and nothing more.
Context: The same subagent that helps you interactively should guard every pull request headlessly. A GitHub Action runs the reviewer on each PR diff, with the API key coming from secrets — never inlined.
Your task: Show a GitHub Actions workflow that runs the reviewer subagent on every PR's diff via the headless claude -p invocation, reading the API key from secrets. Mark this rung as needing an API key.
Requirements:
- Trigger the workflow on pull-request events
- Check out with full history so the PR diff can be computed against main
- Read the API key from repo secrets — never inline it in the YAML
- Compute the PR diff and pass it to a headless
claude -prun - Instruct the run to use the code-reviewer subagent and group findings by severity
- Emit the findings into the CI logs
💡 Hint: Headless means no chat window: claude -p "..." with the diff as input, and the prompt is what points it at your subagent.
Show solution
Headless review in CI — needs the Claude Code CLI + ANTHROPIC_API_KEY secret (this is a GitHub Actions YAML):
name: Claude review
on: [pull_request]
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- env:
ANTHROPIC_API_KEY: "${{ secrets.ANTHROPIC_API_KEY }}" # never inline
run: |
git diff origin/main...HEAD > /tmp/diff.txt
claude -p "Use the code-reviewer subagent on /tmp/diff.txt. \
Output findings grouped by severity." > review.md
cat review.md
The same reviewer subagent runs interactively for a developer and headlessly (claude -p) on every PR — one encoded judgment, two entry points. The key comes from CI secrets, never the file, so the automation stays safe to commit and share.
Context: As tech lead, the leverage is institutional: commit the whole workbench so the team inherits your judgment. This milestone verifies every piece is present and maps each to one of the 4 Ds (Delegation, Description, Discernment, Diligence).
Your task: Write a completeness check that verifies every workbench piece is present and maps each to the 4 D it serves, returning whether the workbench is ready and what's missing.
Requirements:
- Enumerate the pieces: CLAUDE.md, permissions, Stop hook, Skill, subagent, MCP server, CI review
- Map each piece to the D it serves (Delegation / Description / Discernment / Diligence)
- Take a repo-state record and return a ready flag plus a list of missing pieces
- List missing pieces in human-readable form (piece + the D it serves)
- Run it on a complete repo and a partial repo to show it catches gaps
- Explain the leverage: committing
.claude/makes your judgment enforceable team-wide
💡 Hint: A dict mapping each piece to its D, plus a comprehension that collects pieces the repo lacks, gives you both the verdict and the punch-list in one pass.
Show solution
The completeness gate mapped to the 4 Ds (pure stdlib, runnable):
PIECES = { # piece -> which of the 4 Ds it serves
"claude_md": "Description (intent travels with the repo)",
"settings_permissions": "Delegation (allow safe, deny risky)",
"stop_hook": "Discernment (tests verify every change)",
"skill": "Description (house conventions encoded)",
"subagent": "Delegation (least-privilege specialist)",
"mcp_server": "Delegation (domain tools, write-once)",
"ci_review": "Diligence (automated, on every PR)",
}
def workbench_ready(repo):
missing = [f"{k}: {d}" for k, d in PIECES.items() if not repo.get(k)]
return (not missing), missing
full = {k: 1 for k in PIECES}
print("ready:", workbench_ready(full)[0]) # True
partial = dict(full); partial["ci_review"] = 0
print("missing:", workbench_ready(partial)[1]) # Diligence gap
Industry scenario: ten engineers each configure Claude locally and inconsistently. Committing the .claude/ tree + MCP server makes the workbench institutional leverage: your judgment, encoded once, enforced for everyone. The 4 Ds are the checklist — delegate with scoped permissions, describe intent in CLAUDE.md/Skills, discern quality with hooks, and stay diligent with CI review.
✓ Checkpoint — you can move on when you can…
- Assemble CLAUDE.md, settings+hook, Skill, subagent, MCP server.
- Automate verification with a Stop hook and CI review.
- Explain where each of the 4 Ds shows up.
- Ship the workbench so the team inherits it.
| Dimension | Meets the bar | Above the bar (staff) |
|---|---|---|
| Tooling completeness | The repo has CLAUDE.md, a Skill, a subagent, and an MCP server — each present and doing one clear job. | The pieces compose: the subagent uses the Skill and MCP tools, and a fresh git pull gives a teammate the whole working setup. |
| Permission & safety config | settings.json scopes an allow-list and a deny-list; risky actions (git push, vendor edits) are denied so deny wins over allow. | Permissions are genuinely least-privilege, the deny-list is defended against footguns, and dangerous actions stay in human hands by design. |
| MCP tool quality | The MCP server exposes project tools with clear docstrings and typed signatures Claude can route to correctly. | Docstrings are written as routing hints (a vague one picks the wrong tool), and the tools are scoped to what the workbench actually needs. |
| CI review automation | A CI workflow runs a headless Claude review on every PR with the API key read from secrets, output grouped by severity. | The same subagent is reused interactively and headlessly, secrets never touch the file, and the review output is actionable in the PR. |
| Verifiability | A completeness check (Stop hook / workbench_check.py) proves every required piece is present. | The check is a real gate in CI, so an incomplete workbench fails the build rather than shipping half-configured. |
| Ship to the team & the 4 Ds | The whole .claude/ tree + MCP server is committed so teammates inherit it; you can place each of the 4 Ds (Delegation, Description, Discernment, Diligence). | The workbench is institutional leverage: your judgment is encoded once and enforced automatically for every engineer's Claude, verifiably. |
Score each dimension 0 (missing), 1 (meets bar), or 2 (above bar). 0–4: keep building. 5–8: a solid, defensible submission. 9–12: staff-level — you could hand this to a reviewer and defend every call. Any dimension at 0 blocks shipping regardless of the total.
Knowledge check check yourself
Why is git push placed on the deny list in settings.json rather than simply omitted from the allow list?
Show answer
Why does committing the whole .claude/ tree plus the MCP server give more leverage than each engineer configuring Claude locally?
Show answer
git pull makes every teammate inherit the same steering (CLAUDE.md), limits (permissions/deny-list), Skills, subagents, and tools — your judgment is encoded once and enforced automatically across the team, instead of relying on each person to set it up.