Claude Code in Action
Driving Claude Code well over a real, extended task: the steer loop, CLAUDE.md, scoped permissions (with a runnable matcher), verify-don't-trust, and standardizing it for a team.
Claude Code is an AI that lives in your terminal and can read files, run commands, and edit code — in a loop you influence. This chapter climbs from your first steered session to configuring a repo, controlling permissions, verifying work, and setting team standards. Claude Code commands run in your terminal; the Python blocks (a permission model, a verification gate) run offline.
The words you'll hear (in plain terms):
| Term | What it actually means |
|---|---|
| agent | an AI that takes actions in a loop (act → observe → decide), not one reply. |
| steering | guiding the loop: plan approval, scoping, interrupting, rewinding. |
| CLAUDE.md | a project file Claude reads every session: conventions, commands, guardrails. |
| permission mode | what Claude may do without asking (read/edit/run). |
| verification | checking Claude's work (tests, diff) instead of trusting it. |
What you need before starting:
- The C3 intro is the gentle first look.
- Claude Code installed (or read along to learn the model).
- Basic command-line comfort.
New to the topic? Read this box, then take the chapters in order — each section is tagged essential → expert so you always know the depth you're at.
Learning objectives
- Explain why extended sessions drift and how the steer loop counters it.
- Configure a project with CLAUDE.md and scoped permissions.
- Verify work instead of trusting it.
- Set the steering + config standards a team shares.
code/ak1-claude-code-in-action/. Python runs offline; configs are ready to use.1 · Steering beats prompting essential
Here's a moment you'll recognize the first time you use Claude Code seriously. You type a request, hit Enter, and instead of one tidy answer the thing starts moving — reading files, running a command, editing something, running another command. It isn't replying to you; it's working. And the instinct most people bring from chatbots — "craft the perfect prompt, then wait" — is exactly the wrong one here. The engineers who get great results out of Claude Code aren't better prompt-writers. They're better drivers.
Think of the difference between ordering a meal and cooking alongside someone in your kitchen. With a chat model you place an order: one clear request, one dish comes back. With Claude Code you're at the stove together — you say "start the sauce," watch them reach for the wrong pan, say "no, the heavy one," taste as you go, and adjust. You're not composing one flawless instruction up front; you're giving direction continuously, correcting cheaply and often. That ongoing course-correction is what we mean by steering, and it is the single skill this whole chapter is built around.
The common mistake — and it's an expensive one — is to treat plan approval and the interrupt key as optional friction, let the agent run unattended for twenty minutes, and judge it only at the very end. That isn't delegating; it's gambling. When it drifts (and on a long task it will), you've burned the twenty minutes and now have to untangle a large wrong diff instead of nudging a small one. Steering is cheap precisely because you do it early and often — a sentence now beats a revert later.
With a chat model you write one good prompt. With Claude Code you run a loop you influence: approve a plan, narrow scope, interrupt when it drifts, rewind when it goes wrong. The skill is steering, not one-shot prompting.
This is the whole idea of Claude Code in one picture. Unlike a chatbot where you send one prompt and get one answer, working with Claude Code is a loop you stay inside of and keep nudging. Read the five boxes left to right, then notice it repeats.
- You: goal — you start by saying what you want (your intent), e.g. "add pagination, don't touch auth".
- Plan — before writing any code, Claude proposes an approach. This is where a wrong assumption is cheap to fix.
- Act (edit/run) — Claude uses its tools: it edits files and runs commands in your terminal.
- Verify — you (or a test) check the result: did the tests pass? does the output look right?
- You: steer — you course-correct: approve, narrow the scope, interrupt, or rewind — then the loop runs again.
In short: The arrows never stop at "Act" — they always come back to you. That feedback loop (goal → plan → act → verify → steer → repeat) is what "steering" means, and it's the skill this whole chapter teaches.
2 · The extended-session problem essential
Picture a brilliant new colleague who has no long-term memory. For the first hour they're extraordinary — sharp, fast, exactly on brief. But they can only hold so much in their head at once, and as the day fills up, the earliest things you told them quietly fall out. By hour two they've forgotten you said "don't touch the auth module," and they're confidently editing it, sure they're helping. That colleague is Claude Code on a long task, and the disappearing memory is the context window filling up.
This matters because agentic coding feels most powerful exactly where it's most fragile — the big, sprawling, multi-file jobs you'd most love to hand off. A one-line fix almost never drifts; there's nothing to forget. It's the two-hour, twenty-file refactor where early constraints scroll out of memory, the model loses the thread of its own half-finished plan, and small wrong turns compound into a diff you no longer recognize. The failure isn't that the model got dumber; it's that the relevant facts got crowded out by everything that happened since.
The mistake beginners make is to blame the model ("it ignored me!") when the real cause is structural — you asked one context window to hold more than it can. Naming this drift and understanding why it happens is what turns it from a mystery into something you can manage: break the work into scoped chunks, restate constraints as you go, keep a CLAUDE.md the agent re-reads (§4), and verify at each stage rather than at the bitter end. Understanding the loop's lifecycle is what lets you steer a long task without it going sideways.
A one-line prompt is easy. A two-hour refactor across twenty files is where it drifts: the context window fills, early decisions get forgotten, the agent wanders. Understanding the loop's lifecycle is what lets you steer a long task without it going sideways.
Read the top row left→right as time: the leftmost box is what you said first, the rightmost is what happened most recently. Everything shares one fixed-width box — the context window.
- The red box on the far left is your early constraint, "don't touch auth" — the load-bearing rule you set at the start.
- The blue and amber boxes are everything that piled up since: the plan, each edit, each chunk of tool output. They keep arriving and they take room.
- The downward arrow shows the early rule getting pushed off the end ("scrolled out"): once the window is full, the oldest facts fall away first.
- The bottom red box is the payoff — with the rule gone, the agent confidently edits auth. That is drift: not the model getting dumber, just the relevant fact crowded out.
In short: A fixed window + a long session = early constraints scroll away. The fix is to re-supply them (scoped chunks, restated rules, a CLAUDE.md re-read every session).
3 · Plan first, then act essential
Every carpenter you've ever heard quoted the same rule: measure twice, cut once. The reason isn't fussiness — it's arithmetic. A wrong measurement costs you a pencil mark to fix; a wrong cut costs you the board. Claude Code has exactly the same asymmetry, and plan-first is how you exploit it. A wrong idea in a plan costs one sentence to correct. That same wrong idea, discovered after it's been turned into twelve edits across five files, costs you a revert, a re-explanation, and your afternoon.
Here's why this feels counterintuitive at first. The whole appeal of an agent is that it just does the thing — asking it to stop and describe its approach feels like putting a leash on the very speed you paid for. But the plan is where the cheap thinking happens. When Claude says "I'll extract the retry logic into a new decorator and update the three call sites," you can catch in five seconds that it missed a fourth call site, or that your team already has a retry helper it should reuse instead. Neither of those is obvious from the outside once the code is written — they're buried in a diff. On the plan, they're one line each.
The mistake to avoid is treating "make a plan" as a vague ritual — asking for a plan and then rubber-stamping it without reading. A plan you don't scrutinize is worse than no plan, because it lulls you into trusting a direction you never actually checked. Read it like a code reviewer: which files, in what order, what's the risky step, what could this break? Correct the approach before the first edit, when redirection is free. For anything non-trivial, get a plan before code — it catches wrong assumptions when they're cheap to fix.
Two paths for the same wrong idea, split by the dashed line. Read each column top→bottom: the wrong idea appears, then you see what fixing it costs.
- The left column catches the mistake while it is still a plan — just words. The green box shows the fix is one sentence ("use the existing helper, not a new one").
- The right column lets the same mistake become code first: twelve edits across five files (red). Now the fix is a revert, a re-explanation, and lost time.
- The dashed divider is the moment of the first edit — the whole game is to correct the direction on the left of that line, before any code exists.
In short: A wrong measurement costs a pencil mark; a wrong cut costs the board. Read the plan like a reviewer and redirect before the first edit.
A well-steered task
- State the goal and constraints ("add pagination; don't touch auth").
- Ask for a plan; read it. Correct a wrong step now, not after 12 edits.
- Approve; let it work in a scoped area. Watch the first tool calls to confirm direction.
- Interrupt (Esc) and re-steer if it drifts: "stop — wrong file."
- Verify yourself (run tests, read the diff). Only then move on.
steering.md> Plan a refactor that extracts the retry logic from api.py into a reusable
> decorator. List the files you'll touch and the risks. Do NOT write code yet —
> I'll approve the plan first.
# Claude explores, proposes an approach, and waits. You approve or correct
# BEFORE any edits. Enter plan mode explicitly, or just ask for a plan.
This is what you actually type to Claude Code to make it plan before it touches any code. The > at the start of each line is just the prompt shown in the terminal — you type the words after it. The whole message is one instruction.
- The first two lines state the goal precisely: extract the retry logic from
api.pyinto a reusable decorator. A vague goal gets a vague plan. - "List the files you'll touch and the risks" asks Claude to show its thinking first so you can catch a bad idea before it happens.
- "Do NOT write code yet — I'll approve the plan first" is the key line: it puts Claude in plan mode, so it explores and proposes but waits for your OK before editing.
- The
# commentunderneath is a note for you, the reader — it explains that Claude will explore, propose, and pause. You approve or correct before any files change.
What the output means: Claude replies with a written plan (files, steps, risks) and then stops. Nothing on disk changes until you say go.
Try this: Whenever a task is bigger than a one-line fix, add "plan first, don't code yet" to your request. Reading a plan takes seconds; undoing twelve wrong edits does not.
4 · Configure the project — CLAUDE.md intermediate
Think about the onboarding doc a good team hands a new hire — the one-pager that says "we use pnpm, not npm; tests live next to the source; never touch the generated folder." Without it, the new engineer is competent but keeps tripping the same invisible wires: running the wrong command, editing a file that gets overwritten on build, adding a dependency the team spent a meeting deciding against. You could explain all of it out loud every morning. Or you could write it down once. CLAUDE.md is that written-down onboarding doc — except your new colleague reads it fresh at the start of every session, and never forgets to.
This is the highest-leverage file in the whole chapter, and the reason is the drift problem from §2. A chat instruction lives only in the current conversation; when the context window fills, it's gone. A fact in CLAUDE.md is re-supplied every session, so it survives the memory churn that erodes everything else. That's the difference between a rule you have to keep repeating and a rule that just holds. Put your commands there and Claude runs your real pytest -q instead of guessing at a test command; put your guardrails there and "don't push without asking" becomes a standing constraint rather than a hope.
The classic mistake pulls in the opposite direction: treating CLAUDE.md as a wiki and dumping the entire architecture, a history of the project, and three paragraphs of prose into it. That backfires, because every line you add is read into the context window every session — a bloated file spends your budget and dilutes the signal, burying the load-bearing "never edit vendor/" under noise. The skill is editorial: include only the stable facts Claude otherwise rediscovers or gets wrong, and cut everything else. CLAUDE.md is project memory Claude reads every session: conventions, commands, and constraints. It's the single highest-leverage file for quality — it stops you re-explaining context every time and makes the whole team's Claude behave consistently.
Follow the arrows out of the file on the left; then compare the two sides of the dashed line.
- The left box is
CLAUDE.md— your stable facts (commands, conventions, guardrails), written down once. - The three arrows fan out to session 1, 2, 3: the file is re-read at the start of every session, so the same facts are always present no matter how many sessions you run.
- The right side, past the divider, is a plain chat instruction: it lives in one conversation only, and the red note shows it is gone the next session — the drift problem from §2.
In short: Put a fact in a chat and you repeat it forever; put it in CLAUDE.md and every session gets it automatically. Keep it short — every line is re-read into the window.
CLAUDE.md# CLAUDE.md — read at the start of every session
## Commands
- Test: `pytest -q`
- Lint: `ruff check .`
- Run: `python -m app`
## Conventions
- Python 3.12, type hints on public functions.
- Never edit files under `vendor/` — generated.
- Prefer stdlib; a new dependency needs a note in the PR.
## Guardrails
- Do NOT commit or push unless I explicitly ask.
- Ask before deleting files or changing the DB schema.
CLAUDE.md is a plain text file you put in your project. Claude Code reads it automatically at the start of every session, so it's how you tell Claude the things you'd otherwise have to repeat every single time — your commands, your rules, your guardrails.
- The
## Commandssection lists how your project runs its test, lint, and start commands. Now Claude runspytest -qinstead of guessing. ## Conventionsencodes house style: Python 3.12, type hints, never editvendor/, prefer the standard library. These stop Claude from writing code that doesn't fit your repo.## Guardrailsare hard don't rules: don't commit or push unless asked; ask before deleting files or changing the database. These are the safety limits.- The headings (
##) are Markdown — just a readable way to organize the file. Claude reads the whole thing as context, so clear, short bullets work best.
Try this: Add one line to the Conventions section for a rule you keep repeating to Claude. That one line means you never have to say it again — and everyone on your team gets it too.
5 · Permissions — control what it can do intermediate
Imagine giving a new intern the keys to the office. You don't hand over the master key that opens the server room, the safe, and the CEO's desk — you give them a badge that opens the doors they actually need, and leave the rest to require a manager's swipe. Claude Code's permission system is exactly that badge. By default it's cautious: it stops and asks before running a command or editing a file. Permissions are how you say "these doors are always fine, walk right through — but these ones, never, and anything else, come find me."
Why bother tuning it? Because the default "ask every time" is safe but slow, and constant interruptions train you to stop reading the prompts and just mash "yes" — which is less safe than a thoughtful allowlist. The goal is to make the frequent, harmless actions frictionless (running your tests, reading files, editing your own source) so that when a prompt does appear, it's rare enough that you actually pay attention. You're spending a few minutes of setup to buy back both speed and vigilance. It's the same trade an ops team makes with least-privilege IAM: grant exactly what's needed, deny the irreversible, review the unusual.
The mistake here is understandable and dangerous: opening the allowlist too wide because the prompts are annoying — Edit(*), Bash(*), allow-everything. That hands the intern the master key to stop them knocking. The discipline is to be generous with the reversible and read-only (reads, tests, lint, scoped edits) and stingy with the irreversible (force-deletes, pushes, schema changes) — and let everything you haven't thought about fall through to "ask." Claude Code asks before running commands or editing, governed by a permission mode + an allow/deny list in settings.json. Tightening this lets it move fast on safe actions while gating dangerous ones.
Read the three panels left→right — they are the three possible verdicts for any action Claude wants to take.
- The green ALLOW panel holds the frequent, harmless, reversible actions — reading files, running tests, editing your own
src/. These run with no prompt so you aren't trained to mash "yes". - The red DENY panel holds the irreversible ones — force-deletes, pushes, editing generated
vendor/. These are always blocked. - The amber ASK panel in the middle is the safe default: anything not on either list hands the decision to a human, so novel or risky actions never slip through silently.
In short: Sort by "how bad if this is wrong", not "how annoying is the prompt". Generous ALLOW + stingy DENY + default ASK buys back both speed and vigilance.
settings.json{
"permissions": {
"allow": ["Bash(pytest:*)", "Bash(ruff:*)", "Read(*)", "Edit(src/**)"],
"deny": ["Bash(rm -rf:*)", "Bash(git push:*)", "Edit(vendor/**)"]
}
}
This file (.claude/settings.json) is where you decide what Claude may do without stopping to ask you. It's JSON — a list of names and values in curly braces. Two lists do all the work: allow and deny.
"allow"lists safe actions Claude can take freely.Bash(pytest:*)means "anypytestcommand";Read(*)means "read any file";Edit(src/**)means "edit anything undersrc/". The*is a wildcard — it matches anything."deny"lists actions Claude must never do:Bash(rm -rf:*)(dangerous deletes),Bash(git push:*)(pushing code), andEdit(vendor/**)(editing generated files).- Anything not on either list falls through to "ask me first" — the safe default. You widen the allow-list as you learn which actions are safe.
What the output means: With this file in place, Claude runs tests and edits src/ without interrupting you, but stops and refuses to push or delete.
Try this: The next lab turns these exact rules into runnable code so you can see how a single action gets matched to ALLOW, DENY, or ASK.
6 · Advanced — model the permission decision advanced
You've written the allow and deny lists. But have you noticed you're trusting them without knowing exactly how they combine? Here's the question that catches people out: what happens when an action matches both lists — say Edit(vendor/x.py) when your allow-list has Edit(src/*) but your deny-list has Edit(vendor/*)? Or when it matches neither? "It'll probably do the safe thing" is not an answer you want to be guessing at when the action is rm -rf. The only way to trust a security rule is to be able to state precisely what it does in every case — so we're going to build the decision as a few lines of Python you can actually run.
Writing the rule out as code does two things. First, it removes ambiguity: the order of the checks is the policy, and once it's code there's no room for "I think it works like…". Second, it reveals that this isn't some Claude-specific quirk — it's the same pattern every serious access-control system uses. A firewall checks its deny rules before its allow rules; a cloud IAM policy lets an explicit Deny override any Allow; a good bouncer's list of who's barred beats the list of who's invited. Modeling Claude's permission check teaches you a principle you'll reuse far beyond this tool.
The trap this section defends against is the seductive-but-wrong ordering: "check allow first, and if it's allowed, let it through." Do that and a broad Edit(src/*) could green-light something you meant to forbid, because you never gave the deny-list a chance to speak. Deny must be checked first, and unmatched actions must default to ASK — those two decisions are the entire difference between a safe gate and a leaky one, which is exactly why they're worth making explicit in code. How does an allow/deny list actually decide? Modeling it makes the rules unambiguous — and it's the logic a policy check would run.
Follow one action left→right through the two gates; each "no" pushes it to the next check.
- The action enters on the left and hits the red "in deny?" gate first. A match drops straight down to DENY — this is why "deny wins" even if the same action is also allowed.
- If it survives deny, it reaches the green "in allow?" gate. A match drops to ALLOW and runs without a prompt.
- If it matches neither list, it falls through to the amber ASK box on the right — the safe default that hands the call to a human.
- The order of the two gates is the whole point: swap them and a broad allow could green-light something you meant to forbid.
In short: deny → allow → ask, in that fixed order. It is the same precedence a firewall or IAM policy uses: an explicit deny beats any allow.
permission_model.pyimport fnmatch
def decide(action, allow, deny):
"""deny wins over allow; unmatched -> ask the human."""
if any(fnmatch.fnmatch(action, d) for d in deny):
return "DENY"
if any(fnmatch.fnmatch(action, a) for a in allow):
return "ALLOW"
return "ASK"
allow = ["Bash(pytest*)", "Read(*)", "Edit(src/*)"]
deny = ["Bash(rm -rf*)", "Bash(git push*)", "Edit(vendor/*)"]
for act in ["Bash(pytest -q)", "Edit(src/app.py)", "Edit(vendor/x.py)",
"Bash(rm -rf /)", "Bash(curl evil.com)"]:
print(f"{decide(act, allow, deny):5} {act}")
ALLOW Bash(pytest -q)
ALLOW Edit(src/app.py)
DENY Edit(vendor/x.py)
DENY Bash(rm -rf /)
ASK Bash(curl evil.com)
This tiny Python program is the decision logic behind the allow/deny lists you just saw — written out so the rules are unambiguous. Given an action, it returns one of three answers: DENY, ALLOW, or ASK.
fnmatchis a standard-library tool that matches text against a wildcard pattern (soBash(pytest*)matchesBash(pytest -q)). It's the same idea as the*in the settings file.- The function checks the deny list first: if any deny pattern matches, it returns
DENYimmediately. This is the "deny wins over allow" rule from the docstring. - Only if nothing denied it does it check
allow→ALLOW. If neither list matches, it falls through toASK— hand the decision to a human. - The
forloop at the bottom runs five sample actions throughdecide()and prints the verdict for each, lined up in a neat column.
What the output means: Five lines: pytest and editing src/ are ALLOW; editing vendor/ and rm -rf / are DENY; the unknown curl command is ASK — exactly matching the settings you'd expect.
Try this: Add "Bash(curl*)" to the deny list and re-run. The curl line flips from ASK to DENY — proof that adding a deny rule tightens the gate.
7 · Advanced — verify, don't trust advanced
There's a specific sinking feeling you only get once, and then never forget. The agent has been working for a while, it announces "Done — all tests pass, the refactor is complete," and you, relieved, commit it and move on. An hour later something is broken that has nothing to do with what you asked for, and when you finally read the diff you find it quietly rewrote a file you never mentioned. It didn't lie to you. It genuinely believed it was done. "Done" from an agent is a claim, and a claim is not evidence.
This is the habit that separates people who ship agentic work reliably from people who occasionally get burned by it: verify the work with something the agent didn't author. The reason is subtle. The agent's summary is generated by the same process that did the work, so if its understanding was wrong, its report will be confidently wrong in exactly the same way — the narration inherits the bug. Green output from an independent test run, and your own eyes on the diff, come from outside that loop. That's why "I ran the tests and they pass" (from the agent) and "the tests pass" (from you watching the exit code) are completely different levels of trust.
The failure mode to name is verification theater: glancing at the agent's cheerful summary, seeing the word "passed," and calling it checked. That's not verification, it's reassurance. Real verification is boringly concrete — run the suite yourself and read the exit code, run git diff --stat to confirm only the files you expected changed (this is where silent scope-creep shows up), and read anything security- or data-sensitive by hand. The single habit that makes agentic coding reliable: verify the work. An agent that says "done" has proven nothing until you see green tests and a clean diff.
The dashed line splits two sources of "it works": what the agent says (left) versus what you can independently observe (right).
- The left box is the agent that did the work; its arrow feeds the red "done — all pass" summary. Note the red caption: because the report is generated by the same process, if the work was wrong the report is wrong the same way.
- The right side, past the divider, is verification from outside that loop: a green box for an independent test run, another for your own read of the diff.
- The green "real evidence" label is the payoff — trust comes from the right side, never the left. "I ran the tests" (agent) and "the tests pass" (you watching the exit code) are different levels of trust.
In short: Don't accept the narration; run the suite yourself and read git diff --stat. Evidence has to come from outside the loop that wrote the code.
verify_gate.pydef accept_change(tests_pass, diff_reviewed, touched_forbidden):
reasons = []
if not tests_pass: reasons.append("tests not green")
if not diff_reviewed: reasons.append("diff not reviewed")
if touched_forbidden: reasons.append("touched a forbidden path")
return (not reasons), reasons
print("accept?", accept_change(True, True, False))
print("accept?", accept_change(True, False, True))
accept? (True, [])
accept? (False, ['diff not reviewed', 'touched a forbidden path'])
The most important habit in agentic coding is verify, don't trust: an agent saying "done" proves nothing. This function turns "is this change acceptable?" into a clear yes/no with reasons, so you can't wave work through on vibes.
accept_changetakes three facts about the change: did the tests pass, did you review the diff, and did it touch a forbidden path?- Each
ifadds a human-readable reason to thereasonslist when something is wrong. No reasons means nothing is wrong. - It returns a pair:
(not reasons)isTrueonly when the reasons list is empty (accept it), plus the list of reasons so you know why if it was rejected. - The two
printcalls test it: one perfect change, one broken change (diff not reviewed and a forbidden path touched).
What the output means: The first line is (True, []) — accepted, no problems. The second is (False, ['diff not reviewed', 'touched a forbidden path']) — rejected, with the exact reasons spelled out.
Try this: Flip the first True to False (tests failing) and re-run — watch "tests not green" appear and the change get rejected. This is how you make "done" mean "actually verified".
8 · Professional — a repeatable session workflow professional
Watch a surgeon and an airline pilot and you'll notice the same strange thing: highly skilled experts running checklists. Not because they've forgotten how to do their jobs — because under pressure, tired, or distracted, everyone skips the "obvious" step, and the obvious step is the one that bites. Everything in this chapter so far has been a technique; this section is about turning those techniques into a habit you run the same way every time, so that a good session isn't something you have to feel inspired to produce.
The reason a fixed loop matters more with Claude Code than with ordinary coding is that the agent moves fast and the temptation to skip steps is constant. On the tenth easy task of the day you'll want to skip the plan, skip reading the diff, let it push. Nine times you'll get away with it; the tenth is the story you tell in the incident review. A repeatable workflow makes the safe path the default path — you pull latest, state the goal and constraints, plan the non-trivial stuff, watch the early tool calls, verify with tests and a full diff read, and commit by hand — so correctness doesn't depend on your discipline holding up at 5pm on a Friday.
The mistake is thinking a workflow is only for beginners and that you'll "just know" when to be careful. Expertise doesn't remove the failure modes; it just makes you faster at hitting them. The loop below is deliberately short enough to actually follow every time — that's the point. Your per-task Claude Code loop
Read the five boxes left→right — this is the whole chapter compressed into one repeatable checklist you run for every task.
- Pull + state goal — start clean and say the goal and its constraints up front.
- Plan-first — for anything non-trivial, get and approve a plan before edits (§3).
- Work scoped — let it run in a bounded area and watch the first tool calls to confirm direction.
- Verify — run the tests yourself and read the full diff, checking no forbidden paths changed (§7).
- Commit by hand — you write the message; the agent never auto-pushes.
In short: Like a surgeon's checklist, the value is running it the same way every time — the tenth easy task is the one that bites if you skip a step.
- Pull latest; state the goal + constraints.
- Plan-first for anything non-trivial; approve/correct the plan.
- Let it work scoped; watch early tool calls.
- Verify: run tests, read the full diff, check no forbidden paths.
- Commit yourself with a clear message (DF2); never auto-push.
9 · Tech-lead — standardize steering across the team tech-lead
Everything up to now made you good at Claude Code. But a team where quality depends on each person's personal discipline is a team with a quality problem waiting to happen. One engineer has a tight CLAUDE.md and always reads the diff; the next has an empty allow-everything config and trusts "done." Their output looks fine right up until the day the second engineer's unattended agent force-pushes over main. The tech-lead's job is to stop relying on virtue and start relying on defaults.
The move that makes this scale is committing a shared .claude/ tree — CLAUDE.md, settings.json, hooks — into the repo itself, so the conventions, guardrails, and permissions arrive automatically the moment someone clones it. Nobody has to remember to set them up, and nobody can drift from them by accident. It's the same insight as linting or CI: you don't ask people to remember to format their code, you make the formatter run on save. Good steering discipline stops being folklore passed between engineers and becomes a property of the repository, which means it holds even as the team grows and turns over.
The trap a lead falls into is assuming the config, once committed, stays healthy — that nobody quietly widened an allow-list or dropped the test hook to unblock themselves on a Friday. Standards decay silently. That's why the payoff of modeling the config as something you can audit in code (the lab below) is so high: a check that fails loudly when a repo is missing its deny-list or its verification hook is what keeps the standard honest long after you wrote it. A lead commits a shared CLAUDE.md + settings.json so every engineer's Claude follows the same conventions, guardrails, and permissions. Consistency scales quality — and a config check keeps it honest.
.claude/ tree once and every clone inherits it; an audit check catches the day someone quietly widens the allow-list.
Start at the repo on the left, follow the fan-out, then note the guard on the right.
- The left box is the shared
.claude/tree —CLAUDE.md,settings.json, and hooks — committed into the repository itself. - The three arrows deliver it to every engineer the moment they clone: A, B and C all get the identical conventions, guardrails, and permissions with nothing to set up and no way to drift by accident.
- The amber "config audit" box on the right is the honesty check — it fails loudly if a repo loses its deny-list or verification hook, because standards decay silently over time.
In short: Don't rely on each person's virtue — ship the config with the repo so the safe defaults arrive on clone, and audit it so they stay.
config_audit.pydef audit_config(cfg):
problems = []
if "deny" not in cfg.get("permissions", {}):
problems.append("no deny-list -> risky actions ungated")
if "Bash(git push*)" not in cfg.get("permissions", {}).get("deny", []):
problems.append("push not denied -> agent could push unreviewed")
if not cfg.get("has_claude_md"):
problems.append("no CLAUDE.md -> conventions not shared")
if not cfg.get("stop_hook_tests"):
problems.append("no test-on-stop hook -> verification is manual")
return problems
weak = {"permissions": {"allow": ["Edit(*)"]}, "has_claude_md": False}
strong = {"permissions": {"allow":["Edit(src/*)"], "deny":["Bash(git push*)"]},
"has_claude_md": True, "stop_hook_tests": True}
print("weak repo:", audit_config(weak))
print("strong repo:", audit_config(strong) or "compliant")
weak repo: ['push not denied -> agent could push unreviewed', 'no CLAUDE.md -> conventions not shared', 'no test-on-stop hook -> verification is manual']
strong repo: compliant
A tech lead wants every engineer's Claude configured safely, not just their own. This function audits a repo's config and returns a list of problems — an automated check that the safety pieces are actually in place.
- It's handed a
cfgdictionary describing the repo's setup, and builds aproblemslist as it finds gaps. - The four checks map to the four safety habits from this chapter: is there a deny-list? is git push denied? is there a CLAUDE.md? is there a test-on-stop hook (automatic verification)?
- Each missing piece appends a plain-English problem explaining the risk, e.g. "no CLAUDE.md -> conventions not shared".
- It's run on two repos: a
weakone (allow-everything, no CLAUDE.md) and astrongone that follows every rule.
What the output means: The weak repo prints a list of three problems; the strong repo prints compliant (its problem list was empty, so [] or "compliant" shows the word instead).
Try this: Delete one good setting from the strong dictionary (say remove stop_hook_tests) and re-run — that repo stops being "compliant" and the matching problem appears. That's the audit doing its job.
.claude/ tree means steering discipline, guardrails, and verification are the default for everyone who clones the repo — not something each engineer re-learns. That's the tech-lead multiplier.Exercise AK1.1 — Steer + configure a real task
Context: This is the full loop of a well-run Claude Code session in miniature: steer the task, pin down conventions and permissions in config, plan before editing, and refuse to accept changes until they're proven.
Your task: Take a small refactor and run it end to end — a scoped CLAUDE.md and settings.json, plan-first execution with one mid-course re-steer, and a green-tests-plus-diff-review gate before accepting.
Requirements:
- The
CLAUDE.mdincludes the project commands and at least one "don't touch" rule - The
settings.jsonis scoped — read/test tools allowed, irreversible actions handled - Run it plan-first and interrupt once to re-steer mid-task
- Require a green test run and a diff review before accepting the change (use the
accept_changelogic) - Run
config_auditon your setup and address what it flags
💡 Hint: Do the smallest real refactor you can find — the value is in exercising the whole steer → configure → plan → verify loop, not the size of the change.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: In day-to-day Claude Code work, the difference between a session that lands and one that drifts is almost always how the request was framed — steering, not one clever prompt, is the real skill.
Your task: Rewrite a vague ask like "make the tests better" into a steered instruction that names the file, the specific gap, and the guardrails.
Requirements:
- The rewrite names a concrete file (or files) to work in
- It states the specific goal, not a mood — e.g. which missing test paths to add
- It sets an explicit constraint / no-go zone (e.g. don't touch production code, no new dependencies)
- The result is verifiable — you can check whether Claude did exactly that
- Contrast the vague and steered versions side by side so the difference is visible
💡 Hint: Think about what a teammate would need to execute the task without asking a follow-up question — file, gap, and guardrail.
Show solution
Vague prompts invite drift; steered instructions bound the work:
Vague: "make the tests better"
Steered: "In tests/test_auth.py, add cases for the expired-token and
missing-header paths. Use the existing pytest fixtures; don't
touch the production code or add new dependencies."
Naming the file, the specific gap, and the guardrails ("don't touch production code") turns an open-ended request into one Claude can execute without guessing — and one you can verify. Steering is giving direction throughout, not a single perfect prompt up front.
Context: Reviewing a plan is far cheaper than reviewing a diff: catching a wrong approach before any code exists costs one sentence instead of a revert, which is why the plan gate is the intermediate habit worth building.
Your task: Describe the plan-first workflow for a multi-file change and explain why gating on the plan matters before any edit is made.
Requirements:
- Show how to enter plan mode in the Claude Code CLI (and note UI shortcuts drift — verify in the docs)
- The plan is requested explicitly as plan-only: list files and approach, do not edit yet
- You read and correct the approach before code is written, not after
- Only after approval does Claude execute the agreed plan
- Explain the payoff: redirecting a plan is cheaper than reverting a diff
💡 Hint: The key insight is when you intervene, not the exact keystroke — put the review before the first edit.
Show solution
The plan-first loop, in practice (Claude Code CLI):
1. Enter plan mode (Shift+Tab to cycle to "plan", or start with a
"plan only, don't edit yet" instruction).
2. Ask: "Plan how to add rate limiting to the API layer. List the files
you'd change and the approach — do not edit yet."
3. Read the plan. Correct the approach BEFORE code exists
("use the existing middleware, not a new decorator").
4. Approve; Claude executes the agreed plan.
Reviewing a plan is far cheaper than reviewing a diff — you catch a wrong approach before any code is written, when redirection costs one sentence instead of a revert. (Exact key to toggle plan mode: verify in the Claude Code docs, as UI shortcuts drift.)
Context: A CLAUDE.md is read every session, so every line spends context; a tight, load-bearing one steers Claude away from the mistakes it keeps making, while a bloated one dilutes the signal.
Your task: Write a focused CLAUDE.md that captures the conventions Claude keeps getting wrong, and explain what does not belong in it.
Requirements:
- Capture stable, repeatedly-needed facts: tooling/package manager, test layout and command, the fallible-return convention
- Include an explicit Do not section (e.g. don't add deps without asking, don't edit generated code)
- Keep it short — state why a bloated file hurts because it is read every session
- Explain what to leave out (whole-architecture dumps, prose novels)
- The examples are concrete conventions, not generic advice
💡 Hint: Ask yourself which facts Claude rediscovers or gets wrong every session — those, and only those, earn a line.
Show solution
CLAUDE.md is persistent project steering — keep it tight and load-bearing:
# CLAUDE.md
## Conventions
- Package manager is `pnpm`, never `npm`.
- Tests live next to source as `*.test.ts`; run with `pnpm test`.
- Use the existing `Result<T>` type for fallible functions; don't throw.
## Do not
- Don't add dependencies without asking.
- Don't edit `generated/` — it's built from the schema.
Put stable, repeatedly-needed facts (tooling, conventions, no-go zones) that Claude otherwise rediscovers or gets wrong. Don't paste the whole architecture or a novel — a bloated CLAUDE.md dilutes the signal. It is read every session, so every line costs context.
Context: Permissions are what let Claude act without a prompt on every step; getting the allow / ask / deny precedence right is what makes an agent both fast and safe.
Your task: Write a pure-Python model of the allow / ask / deny decision for a tool call, given an allowlist, a denylist, and a default mode.
Requirements:
- The function takes the tool, an allow set, a deny set, and a default mode
- Denylist wins first — a denied tool is blocked even if also allowed
- Allowlisted read-only tools resolve to allow (run without prompting)
- Anything unmatched falls back to the session's default mode (ask by default)
- Demonstrate all three outcomes with example tool calls
- Note that exact
settings.jsonpermission syntax should be verified in the docs
💡 Hint: Model it as an ordered check: deny, then allow, then default — the order is the whole behavior.
Show solution
Model the precedence the CLI applies (deny wins, then allow, then default):
def decide(tool, allow, deny, default_mode):
# denylist always wins
if tool in deny:
return "deny"
if tool in allow:
return "allow"
# otherwise fall back to the session's default mode
return default_mode # "ask" (default) | "allow" | "deny"
allow = {"Read", "Grep", "Bash(pnpm test)"}
deny = {"Bash(rm -rf *)", "Bash(git push)"}
print(decide("Read", allow, deny, "ask")) # allow
print(decide("Bash(git push)", allow, deny, "ask")) # deny
print(decide("Edit", allow, deny, "ask")) # ask (default)
Read-only tools go on the allowlist so they run without prompting; irreversible ones (force-delete, push) go on the denylist so they are always blocked; everything else falls to the default mode. Exact settings.json permission keys/syntax: verify in the Claude Code docs.
Context: "All tests pass" from a long session is a claim, not evidence; the professional habit is closing the loop with a check the agent didn't author, because the real risk in extended runs is silent scope creep.
Your task: Design the verification step for a session that made 12 edits and claims green, so you trust the tooling's output rather than the narration.
Requirements:
- Run the test suite yourself (or have Claude show raw output + exit code), not a summary
- Run the linter as an independent gate
- Use
git diff --statto confirm only the expected files changed - Read the diff by hand for anything security- or data-sensitive
- Only commit on green; on failure, feed the real failing output back and re-steer
💡 Hint: The point is independence — verify with something the agent did not write, and read the diff scope, not just the pass/fail.
Show solution
Close the loop with an independent check the agent didn't author:
# 1. Run the checks yourself (or make Claude run them and show raw output):
pnpm test # exit code + real output, not a summary
pnpm lint
git diff --stat # confirm ONLY the files you expected changed
# 2. Read the diff of anything security- or data-sensitive by hand.
# 3. If green: commit. If not: feed the failing output back and steer.
"All tests pass" is a claim, not evidence — run the suite and read the exit code and diff yourself. The extended-session risk is silent scope creep (files you didn't expect changed), which git diff --stat surfaces immediately. Trust the tools' output, not the narration.
Context: A team standard only works if the safe path is the easy path: irreversible and quality gates belong in shared config so they can't be skipped, while judgement steps stay advisory.
Your task: As tech lead, define a repeatable per-task Claude Code workflow as a checklist a new engineer can run, marking each step enforced vs advisory.
Requirements:
- Written as an ordered per-task checklist (branch first, steer, plan, work, verify, commit/PR)
- Each step is labelled enforced or advisory
- Irreversible/quality gates are enforced in shared config — hooks,
settings.jsonpermissions, branch protection - Judgement steps (steering, planning) are kept advisory
- The standard ships with the repo via version-controlled
CLAUDE.md+settings.json - Note that exact hook/settings keys should be verified in the docs
💡 Hint: Split the checklist by what a human can forget (advisory) versus what must never be skippable (enforce it in config).
Show solution
A team playbook that makes good sessions the default:
Team Claude Code workflow (per task)
1. Branch first (never work on the default branch). [enforced: hook]
2. Steer: state file(s), goal, and constraints up front. [advisory]
3. Plan mode for any change touching >1 file; approve plan. [advisory]
4. Let it work; keep read tools on the allowlist, [enforced: settings.json]
push/delete on the denylist.
5. Verify: run tests + lint + `git diff --stat` yourself. [enforced: PreToolUse hook on commit]
6. Commit with a real message; open a PR for review. [enforced: branch protection]
Enforce the irreversible/quality gates in shared config (permissions, a commit-time hook, branch protection) so they can't be skipped; keep the judgement steps (steering, planning) advisory. A shared, version-controlled CLAUDE.md + settings.json makes the team's standard the path of least resistance. Exact hook/settings keys: verify in the docs.
✓ Checkpoint — you can move on when you can…
- Explain drift and counter it with the steer loop.
- Configure CLAUDE.md + scoped permissions.
- Make a verified test run the definition of done.
- Standardize steering/config for a team and audit it.
Knowledge check check yourself
The lesson claims "steering beats prompting" for Claude Code. What is fundamentally different about working with an agent loop versus a chat model, and how does the extended-session problem arise?
Show answer
In the decide permission matcher, deny is checked before allow and unmatched actions return ASK. Why is "deny-wins plus default-ask" called the safe design?