Running a Small Team on a Big Project: Spec-Driven Development with Claude Code

Monday morning. Four engineers in the standup, a Q4 roadmap with fourteen features on it, a PM who already promised half of those to…


Running a Small Team on a Big Project: Spec-Driven Development with Claude Code

Somewhere off the B41

Monday morning. Four engineers in the standup, a Q4 roadmap with fourteen features on it, a PM who already promised half of those to leadership, and now wants delivery dates by Friday. The arithmetic does not work out. One feature per engineer per quarter, two if everything breaks the right way, and that gets you eight features.

I have run this configuration so many times over the years. Different teams, different domains, same arithmetic at the start. Throughput increased by about a factor of two to three over the past year, while headcount remained unchanged. What moved was how we wrote specs and how we let Claude Code split the work the specs described.

Below is the playbook.

This builds on an earlier piece on spec-driven development, which laid out three workflows scaled to the size of the work: vibe coding for small changes, spec-driven development for standard features, and design-driven parallelism for large multi-engineer efforts. That article came at the topic through Kiro and GitHub’s Spec Kit, the IDE-resident tools that package the workflow into structured artifacts (the requirements doc in EARS notation, the technical design, the sequenced task list, the mini specs that decompose a large design into parallel blocks).

What follows is the same three workflows, mostly the second and third, implemented in Claude Code. Claude Code is terminal-resident rather than IDE-resident, and the primitives it exposes (custom skills, deterministic hooks, subagents in isolated git worktrees) change what design-driven parallelism looks like in practice. The methodology is the same. The mechanics are different, and the mechanics are what determine whether a small team can run mini specs in parallel on a regular cadence rather than as a one-off heroic effort.

The other thing that comes into focus when you run this on a small team for a while, which the earlier article touched on without naming, is that decomposition is the part you cannot automate. Kiro will generate a task list. Claude Code will propose a split. Either output needs a human (usually the manager or tech lead) to map it onto the team you actually have: who has touched what, who reviews well together, where the codebase has parts nobody wants to be alone in, and what the realistic split is between human review and agent review on this particular feature. A decomposition written without that information looks fine on paper and runs serially in practice through whichever engineer happens to have the right context.

Concrete shape, before the mechanics. A working example runs through this article: search with autocomplete and filters, four engineers, written as a four-page spec, and then decomposed into seven mini specs (data, search API, filter parsing, search bar, filter panel, autocomplete service, analytics events). Agent-implemented where the work is well-scoped, human-implemented where judgment is required. A full walkthrough sits at the end. Hold this shape in mind while you read the skills, hooks, and review layers; each piece is built around running this kind of decomposition repeatedly without it costing the team a sprint to coordinate.

Why specs, briefly

The earlier article made the full case. The element that matters most for parallel work is the question a good spec answers that bad ones leave open: can two engineers work on different parts of the feature in parallel without their pull requests colliding? The answer determines whether the team gets parallelism or a queue, and the queue is what sets the calendar.

Claude Code does not enforce any of this. The tool rewards specs that already work, which makes good engineering practice cheaper rather than fixing bad practice.

The built-in surface that earns its keep

Claude Code ships a small set of slash commands that map onto spec work. I use five of them constantly.

/init scans the repo and writes a CLAUDE.md at the root. CLAUDE.md gets read at the start of every session in the project, which means anything important enough to repeat belongs there. The auto-generated version is too long and too generic. I trim aggressively and keep four things: the architecture diagram, the build and test commands, the conventions I actually enforce in review, and a pointer to where specs live. Everything else costs tokens on every turn for no return.

/memory opens CLAUDE.md for editing without leaving the session. I run it whenever I notice Claude making the same mistake twice. The hash prefix (# followed by a note) does the same thing faster: Claude writes the line into memory and keeps going without breaking the work.

/agents manages subagents, which live as markdown files under .claude/agents/. Each subagent gets a separate system prompt, a tool allowlist, and a context window of its own. The main conversation receives only the summary, which keeps the verbose work from accumulating in the parent session.

/hooks opens a read-only browser for the configured hooks in the session, organized by lifecycle event. The browser is for inspection. Configuration goes in .claude/settings.json. Hooks are the deterministic layer of this whole system, so they get their own section below.

/plan runs the session in plan mode, where Claude analyzes and proposes without writing files. I run plan mode at the start of any non-trivial work. The output is a plan file named after the prompt, which I can edit and hand back. Claude executes against the version I approved.

Three more earn their keep but get less screen time. /context shows token usage, which matters once long sessions start degrading quality. /compact summarizes the conversation and frees context space, useful before tackling a fresh spec without losing the thread of the current session. /review runs a local PR review against the diff. /ultrareview does the same job in the cloud across multiple agents in parallel, and the two find different bugs, which is why I run both before any non-trivial merge.

Custom skills: the workflow scaffolding

Built-in commands cover the general layer. Spec work needs scaffolding specific to the workflow, which means writing custom skills.

In current Claude Code, custom commands and skills have merged. A markdown file at .claude/commands/spec-new.md and a skill at .claude/skills/spec-new/SKILL.md both create the slash command /spec-new and behave the same way. Skills support more frontmatter and supporting files, so I write everything as a skill now and leave the legacy commands directory empty.

Five skills go in every repo I work on. Each one gets a full SKILL.md below, in the form I actually check into the repo. Drop them under .claude/skills/<skill-name>/SKILL.md and they appear as slash commands the next time anyone on the team starts a session.

A note before the skills themselves. The frontmatter shown below uses the fields I have settled on for my own setup (name, description, argument-hint, allowed-tools). Claude Code's skill format has been moving as the product matures, so check the current docs before pasting these verbatim and add or rename fields as needed. The body of each skill (the part below the frontmatter) is where the actual workflow lives, and that part is portable.

One mechanical detail that does affect the bodies as written: $ARGUMENTS in a skill expands to the full argument string the user typed after the slash command, as a single value. The skills below that take more than one positional argument parse $ARGUMENTS inside the body, rather than relying on $1, $2, and so on. Adjust the parsing if your team prefers a different convention.

/spec-new: write a fresh spec from the template

The first skill writes the artifact every other skill in the chain depends on. The frontmatter limits the toolset to Read and Write so the skill cannot accidentally run code or modify other files. The body is the actual instruction Claude follows when the slash command runs.

---
name: spec-new
description: Create a new spec from the team template. Use when starting work on a feature, before writing any code, to produce a spec the team can argue with in review.
argument-hint: [feature-name]
allowed-tools: Read Write
---
# Create a new feature spec
## Context
- Current branch: !`git branch --show-current`
- Existing specs: !`ls specs/ 2>/dev/null || echo "no specs directory yet"`
- Recent commits on this branch: !`git log --oneline main..HEAD 2>/dev/null || git log --oneline -10`
- Spec index: !`cat specs/INDEX.md 2>/dev/null || echo "no index yet"`
## Your task
Write a new spec at `specs/$ARGUMENTS.md` using the template below. Add a row for the new spec to `specs/INDEX.md` with status "draft". Cross-reference any existing spec the new feature touches.
## Template
```markdown
# Spec:
**Status:** draft
**Owner:**
**Last updated:**
## Context
Why this work, what triggered it, what user or business problem it solves.
One paragraph. Reference the relevant ticket or doc; do not duplicate it.
## Data contracts
The shape of every piece of data this feature reads, writes, or passes
between layers. Schemas, types, field names, units, nullability.
The data shape is the seam that lets two engineers work in parallel,
so be specific.
## User-visible behavior
What the user sees, in plain language, in the order they see it.
One subsection per distinct behavior. Each subsection is testable.
## Failure modes
What goes wrong, what the system does when it does, what the user sees
in each case. Include at minimum: invalid input, upstream service
unavailable, partial failure, concurrent modification.
## Rollout
Feature flag, gradual rollout, dark launch, big bang. What gets
monitored. What the rollback plan is.
## Out of scope
What this spec is explicitly not doing. Read this section before
arguing about scope in PR review.
## Open questions
Each item has an owner and a target resolution date. Empty by the time
the spec is settled.
```
## Rules
1. Keep the spec under four pages. If it runs longer, the feature should be two specs.
2. Write data contracts before user-visible behavior. The data shape constrains the behavior, not the other way around.
3. Failure modes are not optional. A spec without them gets rejected in spec review.
4. Out of scope is not optional. A spec without it generates scope arguments in code review three weeks from now.

/spec-review: audit the spec for parallel-readiness

This skill is read-only by design. The body asks Claude to delegate the audit to a forked subagent so the gap-list output does not eat context in the parent session. Pair it with the Explore agent type if your version of Claude Code supports that delegation pattern.

---
name: spec-review
description: Audit an existing spec for completeness and ambiguity. Run after writing or substantively editing a spec, before declaring it settled.
argument-hint: [spec-name]
allowed-tools: Read Grep Glob Task
---# Audit a spec for parallel-readiness
Delegate this audit to a forked subagent (Task tool, Explore agent type if available) so the verbose gap analysis does not consume context in the parent session. The subagent returns the structured gap list; the parent session passes it back to the human author.
## Inputs
- Spec to audit: !`cat specs/$ARGUMENTS.md`
- Related specs: !`ls specs/`
- Codebase root: !`ls -la`
## Your task
Audit the spec against the checklist below. Return a structured gap list with one entry per gap. Do not edit the spec yourself; the gap list goes back to the human author.
## Checklist
**Data contracts**
- Every field has a type and a unit where applicable
- Nullability is explicit
- Cardinality is explicit for relationships
- Field naming matches existing codebase conventions (use Grep to verify)
- Schema versioning or migration path is named
**User-visible behavior**
- Every behavior subsection is independently testable
- Loading, empty, and error states are described, not just the happy path
- Behavior is described without assuming an implementation
**Failure modes**
- Invalid input is handled
- Upstream service unavailability is handled
- Partial failure is handled
- Concurrent modification is handled
- A user-visible message exists for each failure case
**Rollout**
- Feature flag or rollout mechanism is named
- Monitoring and alerting that will watch the rollout is named
- Rollback plan is named and is actually executable
**Parallel-readiness**
- The spec can be split into 5 to 10 mini specs along surface, journey, or risk
- No two likely mini specs share state without that state being specced first
- Data contracts are settled enough that API and frontend layers can be built against them in parallel
**Out of scope**
- At least three explicit out-of-scope items are listed
- None of them are things the team will quietly add back during implementation
## Output
For each gap, write a block with:
- **Section** of the spec where the gap appears
- **Concern** in one sentence
- **Suggestion** for how to close it
- **Severity**: blocker, major, or minor
If the entire spec is clean, return only `Spec is parallel-ready` and nothing else. Blockers must be fixed before the spec is settled. Majors should be fixed. Minors are flagged for the author's discretion.

/spec-decompose: split into mini specs

This is the skill that turns a settled spec into a roadmap. It runs the three-pass decomposition (surface, journey, risk) and produces both the mini specs themselves and a parallelism map showing which engineer slot can pick up what on which day.

---
name: spec-decompose
description: Decompose a settled spec into mini specs that can run in parallel against subagents in worktrees. Run only after the spec has passed /spec-review.
argument-hint: [spec-name]
allowed-tools: Read Write Grep Glob Task
---
# Decompose a spec into parallel mini specs
Delegate this to a general-purpose subagent (Task tool) so the decomposition reasoning stays out of the parent session's context. The subagent returns the decomposition file; the parent session presents it to the human for review.
## Inputs
- Spec: !`cat specs/$ARGUMENTS.md`
- Codebase structure: !`find . -maxdepth 3 -type d -not -path '*/node_modules/*' -not -path '*/.git/*'`
- Team file (if present): !`cat .claude/team.md 2>/dev/null || echo "no team file"`
## Your task
Read the spec. Propose a decomposition into mini specs that can run in parallel. Write the decomposition to `specs/$ARGUMENTS.decomposition.md`. Update `specs/INDEX.md` to list each mini spec under the parent spec.
The decomposition is a proposal. The team will disagree with parts of it. Make a clear, opinionated first pass rather than a hedged one.
## Three passes
Run all three passes before writing the final decomposition. Each pass produces candidate mini specs. The final list is the union, deduplicated and ordered.
**Pass 1: Surface.** Walk the layers the feature touches: data, business logic, API, frontend, analytics, configuration, infrastructure. For each layer the spec implies work, draft a mini spec scoped to that layer alone. Two mini specs in different layers can run in parallel as long as the contract between them is settled in the spec.
**Pass 2: User journey.** Walk the user-visible behaviors. Each distinct behavior is a candidate mini spec. Where two behaviors share state, the shared state is a separate mini spec that runs first.
**Pass 3: Risk.** Sort the candidate mini specs by blast radius. High: schema migrations, new external dependencies, anything that needs a rollback plan. Medium: changes to existing services, new endpoints with internal callers. Low: net new code that is additive only, isolated frontend components, analytics events.
## Output format
Write the decomposition with this structure:
```markdown
# Decomposition:
## Mini specs
### MS-1:
- **Layer:** <data | business | api | frontend | analytics | config | infra>
- **Risk:** <high | medium | low>
- **Suggested owner:** <human | implementer-agent>
- **Inputs:** contracts this mini spec reads from the parent spec
- **Outputs:** contracts this mini spec produces
- **Acceptance criteria:** testable list, one bullet per criterion
- **Estimate:** hours or half-days
- **Dependencies:** mini specs that must merge before this one can start
(repeat for each mini spec)
## Parallelism map
A table with one column per engineer slot and one row per working day.
Each cell either names a mini spec or is empty.
## Integration risks
Conflicts the integrator subagent will need to handle: shared types,
shared schemas, mini specs that touch overlapping files.
```
## Rules

  1. No mini spec runs longer than two days of estimated work. If it would, decompose further.
  2. High-risk mini specs go to a human owner, not the implementer agent. Mark accordingly.
  3. The dependency graph should not have a critical path longer than half the total estimated time. If it does, the decomposition is wrong; rerun with a different surface split.
  4. Every mini spec must trace back to a section of the parent spec. If it does not, either it is out of scope or the spec is incomplete.

/spec-implement: execute one mini spec

This is the skill the implementer subagent runs against. It is path-scoped where supported, so it only loads when the session is editing source files. Invocation looks like /spec-implement search-autocomplete MS-1, and the skill body parses the two arguments out of $ARGUMENTS. The paths field shown is supported in some versions of Claude Code; remove it if your version does not understand it and the skill will simply load on every session.

---
name: spec-implement
description: Implement a single mini spec end to end. Reads the mini spec, writes the code and tests, commits.
argument-hint: [feature-name] [mini-spec-id]
paths: src/**, tests/**, app/**, lib/**
allowed-tools: Read Write Edit Bash(git *) Bash(npm *) Bash(pytest *) Grep Glob Task
---
# Implement one mini spec
`$ARGUMENTS` is a single string with two whitespace-separated values: the feature name and the mini-spec id. Parse them as `feature` and `mini_spec_id` before doing anything else. Reject the invocation and return an error if either is missing.
For example, an invocation of `/spec-implement search-autocomplete MS-1` arrives as:
```
$ARGUMENTS = "search-autocomplete MS-1"
```
Split on whitespace; first token is `feature`, second token is `mini_spec_id`. Use those values to construct the file paths in the Inputs block below.
Delegate the implementation work to a general-purpose subagent in an isolated git worktree. The parent session orchestrates; the subagent does the actual edits.
## Inputs (resolved by the subagent)
- Parent spec: `specs/.md`
- Decomposition file: `specs/.decomposition.md`
- The mini-spec block matching `MS-<mini_spec_id>`
- Current branch
## Your task
Implement the named mini spec. Do nothing else.
## Process
1. Parse `$ARGUMENTS` into feature and mini-spec id.
2. Read the parent spec for context. Read the decomposition file and locate the mini-spec block.
3. Find the existing code most relevant to the mini spec. If none exists, create the file in the layer the mini spec specifies.
4. Write the implementation. Stay within the contracts the mini spec names.
5. Write tests for every acceptance criterion. A failing test for a missing acceptance criterion is correct. A passing test that does not actually exercise the criterion is wrong.
6. Run the affected package's test suite. If anything outside the mini spec breaks, stop and report.
7. Commit with message: `MS-: `. Reference the parent spec and the mini spec in the body.
## Hard rules
- Do not touch code outside the layer the mini spec names.
- Do not change shared schemas. If the mini spec reveals a shared schema is wrong, stop and report. Schema changes belong in the parent spec, not in an individual mini spec.
- Tests are not optional. Every acceptance criterion gets at least one test that exercises it.
- If the mini spec is ambiguous, stop and report. Do not pattern-match a plausible interpretation.

/spec-verify: map tests to spec coverage

The last skill closes the loop. It reads the spec and the test suite together and produces a coverage map showing which acceptance criteria have tests and which do not. The output goes into the next sprint or into the same PR depending on the size of the gap.

---
name: spec-verify
description: Verify the test suite covers the spec. Produces a coverage map keyed to spec sections.
argument-hint: [feature-name]
allowed-tools: Read Bash(npm test *) Bash(pytest *) Grep Glob Task
---
# Map test coverage to spec coverage
Delegate to an Explore subagent so the test output and grep results stay out of the parent session's context. The subagent returns the coverage map.
## Inputs
- Parent spec: !`cat specs/$ARGUMENTS.md`
- Test results: !`npm test 2>&1 | tail -50 || pytest 2>&1 | tail -50`
## Your task
Read the spec section by section. For each acceptance criterion, find the test that exercises it. Produce a coverage map. Do not write tests; write the report.
## Output
```markdown
# Coverage map:
## Covered

Spec section Acceptance criterion Test file:line Notes
## Gaps
Spec section Acceptance criterion Reason for gap
--- --- ---
## Tests not traced to the spec
Test file:line Best guess at intent
--- ---
```
## Rules
- A test counts as covering a criterion only if reading the test alone tells you what the spec said. A test that passes by accident does not count.
- Tests not traced to the spec are not necessarily wrong. Note them so the team can decide.
- Run the tests first. A test that does not pass cannot be said to cover anything.

That is the full skills folder. Five files, each one short enough to read in a sitting, each one doing one job in the workflow.

Hooks: the layer the model cannot route around

Hooks are Claude Code’s version of what the earlier article called Kiro’s agent hooks: automated actions that fire on lifecycle events in the development workflow. Where Kiro’s hooks fire on file system events inside the IDE, Claude Code’s hooks live in .claude/settings.json and fire on the agent's tool use lifecycle. The practical consequence is that Claude Code hooks can block a tool call before it executes rather than only reacting after the fact. PreToolUse hooks that exit with code 2 stop the tool call cold, and that primitive is what gives the whole layer teeth.

Skills give Claude guidance the model can ignore on any given turn. Hooks run on a deterministic path the model does not see and cannot rewrite.

Four hooks run in every project I ship. They live in .claude/settings.json, checked into the repo, so every engineer who clones inherits them.

The first one is the spec gate, a PreToolUse hook on Write and Edit. The script reads the file path from the tool input, walks up to find the closest spec reference in the active branch’s commit messages or in specs/INDEX.md, and exits with code 2 if it cannot find one. The block message tells the engineer (or Claude) which directories it tried. This sounds heavy-handed and it is, on purpose. The failure mode it prevents is the one where Claude pattern-matches its way into writing code that solves a slightly different problem than the one I asked for, and the mismatch surfaces in code review three days later when the spec author finally reads the diff.

The second is the post-write test runner, a PostToolUse hook on Write and Edit. Async, so the agent loop does not stall on a slow test suite. The hook writes the test output to a file Claude can read. A separate UserPromptSubmit hook tails the test log and appends recent failures to the next prompt I send into the session, which means Claude sees test failures without me copy-pasting them in.

The third is the completion check, a Stop hook of type prompt. One Haiku call. The prompt asks: did the model finish the task it was asked to do, and if not, what is left? The answer goes back into the conversation. This catches the failure mode where Claude reports success on a partial implementation. Cheap to run, fast, surfaces real bugs every week.

The fourth is the session-start spec loader, a SessionStart hook on resume and startup. The hook runs git log to find the active branch, reads the relevant entry from specs/INDEX.md, and injects the spec pointer into the new session. Claude knows what spec is in play before I type the first prompt of the day.

A fifth, optional, sits on PreCompact and writes the transcript to a backup file before compaction summarizes it. I have lost work to over-aggressive compaction once, and once was enough.

Here is the actual .claude/settings.json that wires all five up. The matchers, the timeouts, the model choice for the prompt-type hook are all parts the team has tuned over time.

A quick legend before the JSON, since the file does not allow comments inline. The lifecycle events map to the hooks described above:

  • PreToolUse with matcher Write|Edit → spec gate (script, blocks on exit 2)
  • PostToolUse with matcher Write|Edit → post-write test runner (script, async)
  • UserPromptSubmit → tails the test log into the next prompt (script)
  • Stop → completion check (prompt-type, calls Haiku once)
  • SessionStart with matcher startup|resume → spec loader (script)
  • PreCompact → transcript backup (script, optional)

A caveat before pasting. Hook configuration shape (matcher syntax, prompt-type hooks, model strings) has shifted across Claude Code releases more than skill frontmatter has. The config below works in my current setup (April 2026); cross-check it against the hooks reference for your version before relying on it. The conceptual layout (which lifecycle event runs which check) carries across versions even when the JSON keys do not.

{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/scripts/spec-gate.sh",
"timeout": 5000
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/scripts/run-tests-async.sh",
"timeout": 1000
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": ".claude/scripts/inject-test-failures.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "prompt",
"model": "claude-haiku-4-5-20251001",
"prompt": "Read the conversation. The user asked the agent to do a task. Did the agent finish the task they were asked to do, or did they stop short? Respond in two parts: (1) DONE or PARTIAL, (2) if PARTIAL, what is left in one sentence."
}
]
}
],
"SessionStart": [
{
"matcher": "startup|resume",
"hooks": [
{
"type": "command",
"command": ".claude/scripts/load-active-spec.sh"
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": ".claude/scripts/backup-transcript.sh"
}
]
}
]
}
}

The spec-gate script is the most consequential of the bunch, because it is the only one that blocks. The rest are observational. The script below reads the tool input from stdin (Claude Code passes hook context as JSON on stdin to command-type hooks), checks whether the write is happening inside a source path, looks for an active spec by branch name and by recent commits, and exits 2 with a message if neither lookup finds one.

#!/bin/bash
# .claude/scripts/spec-gate.sh

# PreToolUse hook for Write and Edit. Blocks the tool call (exit 2) if
# the target file is inside a source path and no active spec can be
# found for the current branch.

# Inputs:
# stdin — JSON from Claude Code containing the tool call context.
# We read .tool_input.file_path from it.

# Exit codes:
# 0 — allow the write
# 2 — block the write; stderr is shown to Claude as the reason
set -e
# Read the JSON payload Claude Code pipes in on stdin, then pull the
# target file path out with jq. The // empty fallback returns an empty
# string instead of "null" if the field is missing.
input=$(cat)
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
# Allow writes outside the source-code directories. Tweak the case
# pattern to match your repo's layout (e.g. add server/*, packages/*).
case "$file_path" in
src/*|app/*|lib/*) ;;
*) exit 0 ;;
esac
# Try to find the active spec by branch name. The ${branch##*/} bash
# expansion strips everything up to and including the last slash, so
# "feature/search-autocomplete" becomes "search-autocomplete".
branch=$(git branch --show-current 2>/dev/null || echo "")
spec_name="${branch##*/}"
spec_file="specs/${spec_name}.md"
if [[ -n "$spec_name" && -f "$spec_file" ]]; then
exit 0
fi
# Fall back to scanning the last 20 commits for a spec reference. This
# covers branches that don't follow the feature/ convention
# but still mention the spec they're implementing in commit messages.
if git log --oneline -20 2>/dev/null | grep -qE 'specs/[a-z0-9-]+\.md'; then
exit 0
fi
# Nothing found. Block the write and tell the caller why. Anything
# written to stderr here gets surfaced to Claude as the block reason,
# so make the message actionable.
cat <&2
spec-gate: blocking write to $file_path
No spec found for the current branch.
Looked for: $spec_file
Looked in: recent commits on this branch
Run /spec-new to create a spec, or commit a reference
to an existing spec, then retry the write.
EOF
exit 2

The other three command-type scripts are similar in shape and shorter. The Stop hook is a prompt-type hook rather than a command-type hook, which means Claude Code calls Haiku directly with the prompt in the settings file instead of running a shell script. That keeps the completion check at single-call latency without a script wrapper.

What hooks are doing in this stack is dividing the labor between the model’s judgment and the team’s enforcement. Anything where Claude getting it wrong is recoverable goes through skills. Anything where Claude getting it wrong costs the team an afternoon goes through hooks.

Code review: four layers, four bug classes

Review is where this whole system either pays off or does not. The team has to catch bugs before they hit main, and review is the last gate. Claude Code gives you four layers, each catching a different class of bug.

Layer one runs on every save, through the PostToolUse hooks above. Type errors, broken imports, failing tests, formatter drift: all of it filtered before the diff is even open in a PR. This is not review in the human sense, but it is the cheapest filter and it stops the largest volume of obvious problems. The hook approach matters here because the model cannot decide to skip the lint step the way it might decide to skip a self-review.

Layer two is /review against the local diff. Fast, runs in the session, catches logic errors, missing null checks, mishandled edge cases, the kinds of things a careful human would catch on a first pass. I run it before pushing and again after pushing, because the second run sometimes catches something I missed in the first because I was thinking about something else.

Layer three is /ultrareview, the multi-agent review that runs in the cloud across the full diff in parallel. Slower. Catches bugs that span files: a contract change in one place that breaks a caller three directories away, a test that passes locally but covers the wrong invariant, a rename that left a stale reference somewhere unobvious. Local review and cloud review are not redundant. They look at different scopes and they catch different things.

Layer four is a custom skill, /spec-aware-review. The skill reads the active spec and the current diff side by side and produces a different kind of report. The first three layers all ask whether the code is correct. Spec-aware review asks whether the code matches what the spec said the code would do. Different question, different bug class.

---
name: spec-aware-review
description: Review a diff against the spec it was supposed to implement
argument-hint: [spec-name]
allowed-tools: Read Bash(git diff:*) Bash(gh pr view:*) Grep Task
---
Delegate to an Explore subagent. The subagent reads the spec and the diff, returns the structured report.
## Inputs
- Spec: !`cat specs/$ARGUMENTS.md`
- Current diff: !`git diff main`
- PR description: !`gh pr view --json body -q .body 2>/dev/null || echo "no PR yet"`
## Your task
Compare the diff against the spec. For each section of the spec, identify:

  1. Does the diff implement what the spec calls for?
  2. Are any spec sections completely unimplemented?
  3. Does the diff implement anything the spec does not call for?
  4. Are the spec's failure modes handled?
    Return a structured report with one section per category. Be specific:
    quote the spec line and reference the file and line in the diff.

The bug class layer four catches is the most expensive one in spec-driven development: an implementation that is technically correct, passes all the tests, and quietly does something different from what the spec described. Diff-based review will not catch it because diff-based review never reads the spec. Putting the spec in the review loop is the entire point of this skill.

The full sequence runs about ten minutes of wall clock on a typical PR. Hooks during work, /review before push, /spec-aware-review after push, /ultrareview before merge. The cost is small. The bugs caught in a year pay for it many times over.

Subagents and the parallelism unit

A subagent runs in its own context window. That property is what matters for spec work. When I delegate a unit of work to a subagent, the verbose output (file searches, log dumps, the back-and-forth of a refactor) stays in that subagent and does not eat into the main session.

Two custom subagents go in .claude/agents/. The other agent roles I use (the spec author, the spec reviewer) get covered by the skills above, instructing the parent session to delegate via the Task tool to a built-in agent type (Explore for read-only audits, general-purpose for the decomposition reasoning). Custom subagent files earn their own slot in .claude/agents/ only when the role needs a substantive system prompt that goes beyond a per-skill prompt.

The implementer subagent is the workhorse. Every mini spec the team chooses to delegate gets implemented by an instance of this subagent running in its own git worktree.

---
name: implementer
description: Implements a single mini spec end to end in an isolated git worktree. Invoked by /spec-implement. Stays inside the layer the mini spec scopes; does not touch shared schemas; writes tests for every acceptance criterion.
tools: Read Write Edit Bash Grep Glob
model: sonnet
---
You are an implementer subagent for the team's spec-driven workflow.
# Your role
You receive one mini spec at a time. You read the parent spec for context. You implement what the mini spec asks for, you write the tests for the mini spec's acceptance criteria, and you do nothing else.
You operate in a temporary git worktree on a feature branch. You commit your work. You do not merge; the integrator subagent will produce a merge plan.
# Hard rules
- Stay inside the layer the mini spec names. The mini spec specifies which directory or module you may touch. Writes outside that scope are out of scope and you stop.
- Do not change shared schemas. If the mini spec reveals a shared schema needs changing, stop and report. Schema changes go through the parent spec, not through individual mini specs.
- Tests are not optional. Every acceptance criterion gets at least one test that exercises it.
- If the mini spec is ambiguous, stop and report. Do not pattern-match a plausible interpretation.
# Process

  1. Read the parent spec at specs/<feature>.md and the mini spec block in specs/<feature>.decomposition.md.
  2. Identify the relevant existing code by walking the directory the mini spec scopes you to.
  3. Write the implementation, staying inside the named contracts.
  4. Write or update tests for every acceptance criterion in the mini spec.
  5. Run the test suite for the affected package.
  6. Commit with message: "MS-<id>: <summary>". Reference the parent spec in the body.
    # Stop conditions
    You stop and return control to the parent session when:
    - Tests pass and the mini spec is complete
    - The mini spec is ambiguous or self-contradictory
    - The implementation requires changing a shared schema
    - A test outside the mini spec's scope breaks during your work
    - Any acceptance criterion cannot be satisfied without touching code outside the mini spec's named layer

The integrator subagent runs at the end of a parallel batch. It does not merge, and that distinction matters. Merging is a decision that needs a human. The integrator reads the diffs from each branch and produces a plan that the human runs.

---
name: integrator
description: Reads diffs from a batch of mini-spec branches and produces a merge plan. Looks for conflicts the merge alone would not catch.
tools: Read Bash Grep Glob
model: sonnet
---
You are the integrator subagent for the team's spec-driven workflow.
# Your role
When a batch of mini specs has finished implementation in parallel worktrees, you read all of the resulting diffs and produce a merge plan. You do not run the merge. You produce the plan; a human runs it.
# What you look for
A trivial merge can succeed at the git level and still produce a broken integration. The conflicts you look for are the ones the merge tool will not see.
- **Shared type changes.** Two mini specs both modify a shared type or interface in compatible-looking but semantically different ways.
- **Shared schema changes.** Two mini specs modify the same database schema, API contract, or message format. This should not happen if the decomposition was correct, but it does.
- **Overlapping file changes.** Two mini specs touch the same file at non-overlapping line ranges. Git merges cleanly. The result may not be what either author intended.
- **Coverage gaps from integration.** Each mini spec's tests pass in isolation, but the combined behavior has a path neither mini spec exercises.
- **Drift from the parent spec.** The combined diff implements something different from what the parent spec described, even when each individual mini spec is faithful to its own block.
# Process

  1. Read the parent spec.
  2. Read the decomposition file.
  3. For each mini-spec branch, run `git diff main..` and read the diff.
  4. Build a table of which files each mini spec touches and where.
  5. Identify the five categories above.
  6. Produce a merge plan with: recommended merge order, conflicts to resolve manually before merging, integration tests to add or run, and any spec or decomposition revisions the integration revealed are needed.
    # Output
    ```markdown
    # Merge plan:
    ## Recommended merge order
    Numbered list with rationale.
    ## Manual conflicts
    File-by-file. For each: the conflict, the mini specs involved, the suggested resolution.
    ## Integration tests required
    Tests to add or run that exercise the combined behavior of more than one mini spec.
    ## Spec revisions
    Anything the integration revealed that the parent spec or decomposition got wrong.
    ```
    # Hard rules
    - You do not run merges. You do not push. You produce a plan.
    - If you find a conflict that cannot be resolved without revising the parent spec, the merge plan stops and the spec goes back to the author.
    - Integration tests you recommend must be runnable against the merged state, not against any individual branch.

The worktree part is what makes the parallelism real. Claude Code can run a subagent in a temporary git worktree, an isolated copy of the repository on a separate branch. Two implementers running in parallel cannot stomp on each other because they are operating on different copies of the working tree. When they finish, the diffs come back as branches, and the integrator reads them all together.

Design-driven parallelism: the decomposition pass

Workflow 3 from the earlier article, with Claude Code’s specific machinery underneath. A four-page spec usually decomposes into five to ten mini specs, each one self-contained enough to run against its own implementer subagent in its own worktree. Some are independent. Others carry an ordering constraint, and the ordering constraint is what you want to identify and minimize.

Decomposition takes three passes. By surface, by user journey, by risk.

The surface pass walks the layers a feature touches: data, business logic, an API, a frontend, an analytics event, a config flag. The layers have natural seams. Two engineers can work on the data layer and the API layer in parallel as long as the data shape is settled, and the data shape is settled in the spec, which is what the spec is for.

The user journey pass walks the user-visible behaviors. A feature with five distinct behaviors has five testable units. Where the behaviors share state, the shared state is the bottleneck and gets specced and built first; everything else can run after.

The risk pass sorts by blast radius. Risky pieces (a new database migration, a new external dependency, anything that needs a rollback plan) go to the engineer most familiar with the area. Low-risk pieces go in parallel to everyone else. Most low-risk pieces are scoped tightly enough that an implementer subagent finishes them with light supervision, which is where Claude Code earns the most leverage.

The artifact that comes out of decomposition is the spec index, at specs/INDEX.md. The earlier article called the per-feature version of this a roadmap, the master plan that visualizes parallel blocks and integration points. The spec index is one level up: every spec across the project gets a row, and every row has a status, an owner, the mini specs it has been broken into, the mini specs in flight, and the mini specs blocked. I open this file every Monday morning. The team treats it as the project plan, and so far no project management tool we tried before has matched it for actually communicating what is happening.

The format is a markdown table, plain and short:

# Spec index

Spec Status Owner Mini specs In flight Blocked
search-autocomplete active me MS-1..MS-7 MS-2, MS-4 MS-6 (waits on MS-1)
user-prefs-export settled engineer A MS-1..MS-3 - -
billing-rollup-v2 draft engineer B - - -
auth-mfa-rollout shipped engineer C MS-1..MS-5 - -

That is the whole file. New specs append a row. Mini specs reference their parent spec by name, and the in-flight and blocked columns get updated by whoever moves a mini spec from one state to another. The session-start hook described above reads this file to figure out which spec is in play for the current branch, which is the only piece of automation touching it. Everything else is human edits, and that is on purpose; the index works because the team trusts it, and the team trusts it because the team writes it.

A worked example

Search with autocomplete and filters. The PM hands me a one-pager. I write a four-page spec over a day, run /spec-review against it, fix the three gaps the audit returns, and call the spec settled.

/spec-decompose proposes seven mini specs:

  1. Backing index schema and ingestion job (risky, owner: me)
  2. Search API with query parsing (medium, owner: engineer A)
  3. Filter parameter parsing and validation (low, owner: engineer A)
  4. Frontend search bar component (low, owner: engineer B)
  5. Frontend filter panel (low, owner: engineer B)
  6. Autocomplete service (medium, owner: engineer C)
  7. Analytics events for query refinement (low, owner: engineer C)

I take mini spec 1 first, because the index work is the riskiest piece and I am the one who has touched the indexing system before. While that is in flight, A and B and C start their work in parallel from the spec. The data contracts are written down, so mini spec 2 does not need mini spec 1 to be merged before it can be implemented. A writes against the contract, mocks the index, tests with fixtures.

When mini spec 1 lands, A’s tests run against the real index and A finds two contract violations. We update the spec, update the implementation, move on. The contract violations were going to surface eventually. Finding them in week two of the sprint instead of week five saves the calendar.

Mini specs that take less than half a day go to /spec-implement running in worktrees. I review the diffs, run /spec-aware-review against each one, then /ultrareview before merge. Mini specs that need real engineering judgment stay with the human. The split is roughly sixty-forty in favor of the human, but the forty percent that runs through Claude is what gives the team back its evenings.

What goes wrong

Three things go wrong every time, in some combination, and you should plan for them rather than try to prevent them.

The spec is too vague. Claude fills the vagueness with plausible code that does not match what you wanted, and the mismatch surfaces in spec-aware review or, worse, in production. The fix is to run /spec-review early and not declare a spec settled until the audit comes back clean. The cost of underspeccing compounds over the life of the work; an extra hour at the start saves a day at the end.

The decomposition hides a coupling. Two mini specs that looked independent in the decomposition pass turn out to share state. A forty-five-minute coupling review with the team after /spec-decompose proposes its split usually surfaces these. Four engineers in a room can spot couplings that no static analysis catches, and the cost of the meeting is small compared to the cost of merging two PRs that touch the same shared schema in incompatible ways.

Integration gets messy. The integrator subagent helps but is not a fix on its own. Merging often, in small batches, with the spec index kept up to date, is the only thing that makes two mini specs in flight visible to each other before they collide.

Where to start

If your team is doing this for the first time, do not try to install all of it at once. Start with /init and a trimmed CLAUDE.md. Add one custom skill: a /spec-new that writes a spec from your template. Use it for one feature. Find where the breakdown happens. Add the next skill where the breakdown is.

The skills folder grows the way a team’s runbook grows: slowly, in response to specific frustrations rather than imagined ones. Hooks come last, after the team has agreed on what is worth enforcing deterministically. Ordering matters here, because a hook installed before the team has consensus on the rule it encodes becomes friction the team works around instead of a guardrail the team relies on.

None of this makes any individual task faster. What it changes is throughput. A fourteen-feature roadmap needs more than one feature per engineer per quarter, and four engineers cannot personally write that much code in that time. The rest comes from parallelism, and the parallelism is what specs, skills, hooks, subagents, and review layers add up to.

By Joshua McDonald on April 28, 2026.

Canonical link

Exported from Medium on August 26, 2026.