C2 — Build a Committed .claude/ Steering Kit
Summary — what this page covers In C1 you wrote a
CLAUDE.mdthat loads on every turn. Now you'll split those conventions into a committed.claude/steering kit — path-scoped rules, a skill, a subagent, a guardrail hook, settings, and a plugin — and trimCLAUDE.mddown to a map. To prove the skill works, you'll ship the first reviews endpoint it produces and make it permanent.Time: ~45 minutes · Format: hands-on, with Claude Code · You start from:
checkpoint/c1-claude-md· You end at:checkpoint/c2-steering-kit
C1 put everything in one always-on file. That works, but every convention costs context on every
turn, and a CLAUDE.md "never do X" line is only a suggestion — Claude can still do X. In C2 you
make steering scoped (rules that load only when relevant), reusable (a skill that triggers by
description, a subagent that runs on a cheaper model), and enforced (a hook that blocks
destructive commands). Claude Code itself helps you build the kit — and then uses it.
1. Prerequisites
- Day 1 access: the Claude Code CLI and a paid Claude plan.
jqinstalled — the destructive-command hook parses its JSON input with it (jq --version). Preinstalled on most macOS/WSL dev setups; otherwisebrew install jq.
2. Start from the C1 checkpoint
Each lab starts from the previous checkpoint, and the matching tag is the answer key. C2 starts from
checkpoint/c1-claude-md; checkpoint/c2-steering-kit holds the full as-built files if you get stuck.
# from the repo root, branch off the C1 checkpoint
git switch -c my-c2 checkpoint/c1-claude-md
# everything you run happens inside the solution folder
cd src/BookTracker
# sanity check
dotnet build BookTracker.sln
Peek at the answer key any time without leaving your branch:
git show checkpoint/c2-steering-kit:src/BookTracker/.claude/settings.json
3. Add path-scoped rules
Rules hold the conventions that lived inline in your C1 CLAUDE.md. A paths: glob in the
frontmatter makes each one path-scoped — Claude loads it only when it reads a file that matches,
so the conventions cost context only when they're relevant.
Ask Claude Code to create them, or write them by hand. Create .claude/rules/api-endpoints.md:
---
paths:
- "BookTracker.Api/Endpoints/**/*.cs"
---
# API endpoint conventions
- Keep endpoints thin — business logic lives in Core services, not the handler.
- Accept and return DTOs (in `BookTracker.Core/Dtos`); never return EF entities.
- Validate inputs before they reach the data layer.
- Async all the way; thread the `CancellationToken` through.
And .claude/rules/data-access.md:
---
paths:
- "BookTracker.Data/**/*.cs"
---
# Data-access conventions
- Parameterized queries / EF LINQ only — never build SQL by string concatenation.
- Use the async EF APIs; thread the `CancellationToken` through.
- Schema changes go through EF Core migrations in `BookTracker.Data`.
Gotcha — rules load on read, not create. A path-scoped rule fires when Claude opens a matching file, not when it's about to create one from scratch. Keep creation-time guidance unscoped (in
CLAUDE.mdor the skill). Also keep these project-level — user-level~/.claude/rules/globs are currently unreliable.
4. Add the add-api-endpoint skill (a folder, not a file)
A skill is a folder, not a single file: a SKILL.md whose description is the trigger, plus
supporting references/. Claude auto-applies it when a request matches the description — you don't
invoke it by name.
Create .claude/skills/add-api-endpoint/SKILL.md:
---
name: add-api-endpoint
description: Use when adding a new HTTP endpoint to BookTracker.Api — creating the route,
handler, request/response DTOs, and wiring it into the Minimal API. Triggers on requests like
"add an endpoint", "expose a new route", "create a GET/POST for ...".
---
# Add an API endpoint
## Procedure
1. Read `references/endpoint-pattern.md` and an existing endpoint to match the house style.
2. Define request/response DTOs in `BookTracker.Core/Dtos`.
3. Add the handler and map the route in `BookTracker.Api/Endpoints`.
4. Build, then add a test mirroring `BookTracker.Tests`.
## Gotchas
- Never return an EF entity — map to a DTO.
- Validate inputs before touching the data layer.
- Keep the handler thin; put logic in a Core service.
Then add the reference doc .claude/skills/add-api-endpoint/references/endpoint-pattern.md: a short
markdown page describing the canonical BookTracker endpoint shape — a MapGroup per resource, handler
→ Core service, returns a *Dto, takes a CancellationToken, validates input, returns
Results.Ok/NotFound/Created — and pointing at an existing clean endpoint (GET /books) as the live
example.
5. Add the test-writer subagent
A subagent is a .md file: YAML frontmatter plus a body that becomes its system prompt. Pinning
model: claude-haiku-4-5 is the cost lever — test generation runs on the cheaper model. (You'll use
this subagent for real in C4.)
Create .claude/agents/test-writer.md:
---
name: test-writer
description: Generate xUnit tests for a class, matching the patterns in BookTracker.Tests.
model: claude-haiku-4-5
---
You write focused xUnit tests. Read the target class and the existing tests first,
mirror the house style, and cover the happy path plus the important edge cases.
Return only the test code and a one-line summary.
6. Add the guardrail hook (this is the enforcement)
A CLAUDE.md line can ask Claude not to run something; a hook can stop it. A PreToolUse hook
receives the tool call as JSON on stdin, and exit 2 blocks the call (its stderr is shown to
Claude). Create .claude/hooks/block-destructive.sh:
#!/usr/bin/env bash
# PreToolUse (Bash) guardrail: block destructive commands.
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eiq 'rm[[:space:]]+-rf|DROP[[:space:]]+TABLE|git[[:space:]]+push.*--force'; then
echo "Blocked by guardrail: destructive command pattern detected." >&2
exit 2
fi
exit 0
Mark it executable:
chmod +x .claude/hooks/block-destructive.sh
This is the section's punchline: a
CLAUDE.md"never run X" line is a suggestion; this exit-2 hook is enforcement. It needsjqto read the command out of the JSON input.
7. Register the hooks in settings.json
.claude/settings.json wires the guardrail in as a PreToolUse hook on Bash, and adds a
PostToolUse build hook that runs dotnet build after every Edit/Write — so compile
failures land back in Claude's context and it self-corrects. Create .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash", "hooks": [
{ "type": "command", "command": ".claude/hooks/block-destructive.sh" }
]}
],
"PostToolUse": [
{ "matcher": "Edit|Write", "hooks": [
{ "type": "command", "command": "dotnet build" }
]}
]
}
}
Settings load at session start, so test from a fresh session (start a new
claudesession) after editing this file.
8. Bundle it as a plugin
A plugin packages the rules + skill + hooks so a teammate gets the whole setup in one install;
components are discovered by convention. Create .claude/plugins/booktracker-kit/plugin.json:
{
"name": "booktracker-kit",
"version": "0.1.0",
"description": "BookTracker steering kit: API/data rules, the add-api-endpoint skill, and a destructive-command guardrail."
}
9. Trim CLAUDE.md down to a map
Now move the path-scoped conventions out of CLAUDE.md — they live in .claude/rules/ and load
automatically. Keep the rest (Description · Stack · Commands · Architecture); remove the inline
## Conventions list from C1 and replace it with a pointer:
## Conventions
Path-scoped conventions live in `.claude/rules/` (loaded automatically when Claude works in the
matching files): `api-endpoints.md`, `data-access.md`.
Net effect: the always-on CLAUDE.md gets shorter, and the conventions only cost context when Claude
is actually in Endpoints/** or Data/**.
10. Use the skill: ship the reviews endpoint
C0 deliberately shipped no reviews feature. Now test the add-api-endpoint skill by asking for it in
plain language — the skill should auto-apply by description (no /-command, no naming it):
Add a GET /api/books/{id}/reviews endpoint that returns the book's reviews, newest first.
Claude follows the skill's procedure and the new rules. Make the result permanent — it follows the
house repository pattern (Core ← Data ← Api):
BookTracker.Core/Dtos/ReviewDto.cs—Id,BookId,Reviewer,Rating,Body,CreatedOn.BookTracker.Core/Interfaces/IReviewRepository.cs— persistence port;GetForBookAsync(int bookId, CancellationToken ct)returns entities, newest first.BookTracker.Core/Services/IReviewService.cs+ReviewService.cs— projects entities toReviewDto; clean (no GAP patterns).BookTracker.Data/Repositories/ReviewRepository.cs— async EF LINQ.BookTracker.Api/Endpoints/ReviewsEndpoints.cs—GET /api/books/{id}/reviews→ReviewDto[]newest-first; thin handler →IReviewService; 404 if the book doesn't exist.- Seed a few reviews, add an
AddReviewsEF migration, and inProgram.csregisterIReviewService/IReviewRepositoryand callapp.MapReviewsEndpoints().
As-built gotcha: sort the reviews newest-first client-side — materialize with
.AsNoTracking().Where(...).ToListAsync(ct)thenOrderByDescending(r => r.CreatedOn)in memory. SQLite can'tORDER BYaDateTimeOffsetin SQL, so don't push the ordering into the query.
Build, test, and run to confirm:
dotnet build BookTracker.sln
dotnet test BookTracker.sln
dotnet run --project BookTracker.Api # http://localhost:5255
# in a second terminal
curl http://localhost:5255/api/books/1/reviews # → ReviewDto[], newest first
curl -i http://localhost:5255/api/books/9999/reviews # → 404 for an unknown book
11. Verify the kit, then commit it
Confirm each piece behaves before committing:
-
/memoryshows a path-scoped rule loading only on a matching file — open one underBookTracker.Api/Endpoints/and confirmapi-endpointsis loaded; confirm it's absent when you're working elsewhere. -
The
add-api-endpointskill auto-applied by description in step 10 (you never named it). -
From a fresh session, ask Claude to run a destructive command and watch it get blocked (exit 2) with the reason; then make a real edit and watch the build hook fire.
Everything under .claude/ is committed so the kit travels with the repo:
git add .claude CLAUDE.md
git add BookTracker.Core BookTracker.Data BookTracker.Api
git commit -m "C2: steering kit + reviews endpoint"
✅ Checkpoint — you're done when:
-
dotnet buildanddotnet testare green. -
/memoryshows a path-scoped rule loading only on a matching file. -
add-api-endpointis a folder with aSKILL.md, and it auto-applies by description. -
From a fresh session, a destructive command is blocked (exit 2) and a real edit fires the build hook.
-
GET /api/books/1/reviewsreturns reviews newest-first; an unknown book returns 404. -
CLAUDE.mdis trimmed — no inline conventions; it points at.claude/rules/. -
Everything under
.claude/is committed, includingplugin.json.
Your branch should now match checkpoint/c2-steering-kit.
What's next
Lab 3 (C2 → C3): you'll connect Claude Code to your IDE and add the GitHub MCP server —
and your committed .claude/ kit travels with the repo and keeps applying there (same agent, same
config).