# C2 — Build a Committed `.claude/` Steering Kit

> **Summary — what this page covers**
> In C1 you wrote a `CLAUDE.md` that 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 trim `CLAUDE.md` down 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**.
- **`jq`** installed — the destructive-command hook parses its JSON input with it (`jq --version`).
  Preinstalled on most macOS/WSL dev setups; otherwise `brew 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.

```bash
# 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`:

```markdown
---
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`:

```markdown
---
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.md` or 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`:

```markdown
---
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`:

```markdown
---
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`:

```bash
#!/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:

```bash
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 needs `jq` to 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`:

```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 `claude` session)
> 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`:

```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:

```markdown
## 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):

```text
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 to `ReviewDto`; 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 `AddReviews` EF migration, and in `Program.cs` register
  `IReviewService`/`IReviewRepository` and call `app.MapReviewsEndpoints()`.

> **As-built gotcha:** sort the reviews **newest-first client-side** — materialize with
> `.AsNoTracking().Where(...).ToListAsync(ct)` *then* `OrderByDescending(r => r.CreatedOn)` in memory.
> **SQLite can't `ORDER BY` a `DateTimeOffset` in SQL**, so don't push the ordering into the query.

Build, test, and run to confirm:

```bash
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:

- `/memory` shows a path-scoped rule loading **only** on a matching file — open one under
  `BookTracker.Api/Endpoints/` and confirm `api-endpoints` is loaded; confirm it's absent when you're
  working elsewhere.
- The `add-api-endpoint` skill **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:

```bash
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 build` and `dotnet test` are **green**.
- [ ] `/memory` shows a path-scoped rule loading **only** on a matching file.
- [ ] `add-api-endpoint` is a **folder** with a `SKILL.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/reviews` returns reviews **newest-first**; an unknown book returns **404**.
- [ ] `CLAUDE.md` is trimmed — no inline conventions; it points at `.claude/rules/`.
- [ ] Everything under `.claude/` is committed, including `plugin.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).
