πŸŽ“ Claude Architect KB CCAR-F Β· CCAR-P study guide

Pro Domain 7 Β· Developer Productivity & Operational Enablement (7%)

What this domain asks you to do. The Foundations track taught you what each Claude Code file is: which CLAUDE.md loads, how settings precedence merges, when a hook fires, and what a skill contains. The Professional track asks a different question. You are responsible for forty developers, not one terminal. The exam guide says the minimally qualified candidate "configures Claude tools and environments for teams", "improves developer workflows using AI-assisted tooling", and "supports debugging and operational issue resolution". Each of these verbs is about a team. So the artifacts (the files and documents you produce) are team artifacts too: a managed policy that an IT admin deploys, a shared conventions file that the team reviews in pull requests, a skills catalog with owners, an MCP allowlist, a CI hook, an adoption dashboard, a runbook, and an enablement plan that someone signs. This page builds directly on Domain 2 Β· Claude Code. It links to the mechanisms there and does not teach them again.

πŸ“– New here? Forty developers, one platform team, and the week everything was "working" on exactly one laptop

Meridian Freight builds shipment-tracking software. Three developers tried Claude Code on their own and liked it very much. The CTO then asked the platform team to "roll it out". In week one, each of the forty developers installed it, and each got a different experience. One had a CLAUDE.md that said "always use TypeScript strict mode". Another had none. A third had granted Bash(*) to stop the permission prompts. Nobody could say what the tool was allowed to do.

The architect's first step was not a prompt. It was ownership. Security owns what the tool must never do. So those rules went into a managed policy that IT deploys and that no developer can loosen. The team owns its conventions. So a root CLAUDE.md and a committed .claude/settings.json went through code review like any other file. Each developer owns their own preferences, so nobody changed the user layer.

Next, the workflow changed, and the team measured it. Before, a pull-request review waited two days for a senior engineer. Now a CI job runs headless Claude Code with a token that has limited scope. The job posts findings as advisory comments (suggestions, not decisions). A deterministic test suite still decides whether the merge is blocked. The team tracked four numbers: review wait time, defect escape rate, cost per pull request, and how often developers accepted the suggestions.

Then something broke β€” at 2 AM, as usual. The shipment-ETA assistant started to return refusals. This assistant is a Claude-powered production service, not the coding tool. The on-call engineer did not have to guess. The runbook listed which log fields to collect (request id, stop_reason, model, tool results). The triage flow said to check the last deploy and the retrieval index before changing any prompt. The rollback was one command. The incident took twenty minutes. Its post-mortem became the next section of the runbook.

Those four steps β€” configure, improve, measure, support β€” are this domain. It is 7% of the exam. But every question in it hides a choice between two types of control: policy or preference, advisory or blocking, symptom or cause, detective or preventive.

Objective 1 Β· Configure Claude tools and environments for teams

Configure Claude tools and environments for teams (e.g., Claude Code)

The mechanism is the same ladder you learned in Foundations. Settings precedence works like this: managed policy sets hard limits first, and then CLI flags, local project, shared project, and user settings apply in order. Foundations teaches it in Permissions & settings precedence, and shows the three files side by side in Settings β€” the same key at three layers. At the team level, one thing changes: the question you ask about every line. Who owns this rule, and what happens if that owner is wrong? The answer to that question is a team environment configuration. It is a small set of files with named owners, an order for the rollout, and a way to check that each developer's machine really received them.

Managed settings rollout β€” the policy layer as a team artifact

Who writes it: security or platform engineering. The architect decides what belongs there. Where it lives: the [managed policy](term:managed policy) file on each machine. MDM (device management software) or a setup script deploys it. On macOS it is /Library/Application Support/ClaudeCode/managed-settings.json; on Linux it is /etc/claude-code/managed-settings.json. Developers cannot edit it, and no lower layer can loosen it. A managed settings rollout is the plan to put that file on every machine, in stages, with a check at each stage.

Here is a managed file for Meridian. It is illustrative (an example, not a real company file), but complete:

JSON
{
  "permissions": {
    "deny": [
      "Read(.env*)",
      "Read(**/*.pem)",
      "Read(**/secrets/**)",
      "Bash(curl:*)",
      "Bash(wget:*)"
    ],
    "ask": [
      "Bash(git push:*)",
      "Bash(npm publish:*)"
    ]
  },
  "disableBypassPermissionsMode": "disable",
  "forceLoginMethod": "console",
  "env": {
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_METRICS_EXPORTER": "otlp",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "https://otel.meridian.internal:4318"
  }
}

Read it by owner, not by key. permissions.deny is the security team's list: the tool cannot read secrets, and it cannot run quick commands that send data out of the network. permissions.ask is the "confirm before it leaves the machine" list. disableBypassPermissionsMode removes --dangerously-skip-permissions from every laptop. forceLoginMethod keeps billing on the organization's account. The env block turns on telemetry and sends it to the team's collector. The adoption metrics later on this page come from there. Nothing in this file is a convention. There is no coding style and no "prefer TypeScript". Those belong one layer lower, where a pull request can change them.

The rollout happens in stages. The reason: a wrong policy is wrong on forty machines at the same time.

Stage Who What ships Exit check
Pilot 3 developers who volunteered the managed file with deny only each runs claude and confirms /status shows the managed layer; a Read(.env) attempt is refused
Broad all 40 the same file plus ask and telemetry env telemetry collector sees β‰₯35 distinct sessions in a week
Enforce all 40 disableBypassPermissionsMode added no session in the collector reports bypass mode

Shared CLAUDE.md conventions β€” the layer pull requests review

Who writes it: the team, through code review. Where it lives: the CLAUDE.md at the repository root. Nested folder memory covers sub-areas, and .claude/rules/ holds conventions for specific paths. Foundations teaches the memory hierarchy. For a team, the rule is: one root file, kept short, owned through a CODEOWNERS entry, and changed only by pull request.

A team root file that has worked well for forty developers is short and gives direct commands:

MARKDOWN
# Meridian tracking-api β€” team conventions

- Run `npm test` and `npm run lint` before reporting a task complete. Paste the summary line.
- Never edit files under `migrations/`; propose a new migration file instead.
- API handlers live in `src/handlers/`; shared validation in `src/validate/` (Zod). Do not add a third place.
- Secrets come from the environment. Never write a literal key, token, or password into any file.
- When a task touches `src/billing/`, read `src/billing/CLAUDE.md` first (nested memory).
- Use the `release-check` skill for anything that mentions shipping, deploying, or go-live.

Notice what it does not contain: no security policy (that goes in the managed layer), no permission grants (that goes in settings.json), and no long explanations. Each line names a place, a command, or a skill. If a convention file grows longer than one screen, that is a warning sign. The fix is nested memory in the folder that needs the detail, not a longer root file.

The skills catalog β€” packaged procedures with owners

Who writes it: the person who owns the procedure. Where it lives: .claude/skills/<name>/SKILL.md in the repository (project scope), or ~/.claude/skills/ for personal skills. Skills are procedures, not connections. The difference is explained in Skills vs slash commands vs subagents. A skills catalog is the team's list of skills: name, owner, trigger phrases, and the date someone last checked it. Without the catalog, two developers write the same "deploy" procedure, and a third developer runs the old one.

MARKDOWN
---
name: release-check
description: Run Meridian's pre-release checklist. Use when the user says ship, deploy, release, or go-live.
---

1. Confirm the branch is `main` and the working tree is clean; stop and report if not.
2. Run `npm test` and `npm run lint`; on any failure, report the failing line and stop.
3. Run `node scripts/check-migrations.mjs`; a non-zero exit means an unreviewed migration.
4. Print the exact `git log --oneline -5` and ask the user to confirm before tagging.
Catalog column Why the team needs it
Owner who answers when the skill is wrong
Trigger phrases so the description matches how people actually ask
Scope (project / user) project skills ship with the repo; user skills do not
Last verified a skill nobody has run in three months is a risk

Skills that run in their own subagent β€” context: fork

Some skills make a lot of noise. A skill that searches the whole codebase reads dozens of files. If it runs inside your main session, every one of those file reads lands in your conversation. Your context window (the text the model can see at one time) fills up with search results that you do not need after the answer arrives. That is cross-contamination of session state: the procedure's working notes mix with the conversation you are having.

Here is an illustrative skill file (an example for Meridian, not a real company file):

MARKDOWN
---
name: trace-status
description: Find every place that reads or writes the shipment status field and return a file:line table. Use when someone asks where shipment status is used or changed.
context: fork
agent: Explore
allowed-tools: Read, Grep, Glob
---

Find every read and every write of the shipment `status` field in this repository.

1. Use Glob to list candidate files: `src/**/*.ts`, excluding `**/*.test.ts`.
2. Use Grep for `status` together with `Shipment` to find the matching lines.
3. Read only the matching line ranges, to decide whether each line reads or writes the field.
4. Return one Markdown table: file:line | read or write | one-line note. Do not return file contents.

What each line does, and who acts on it.

Line Who reads it What it does
context: fork Claude Code starts a new subagent and gives it the skill body as its prompt; the subagent does not see the conversation history
agent: Explore Claude Code picks the subagent type: built-in Explore, Plan, general-purpose (the default when agent is omitted), or a custom agent from .claude/agents/
allowed-tools Claude Code pre-approves these tools while the skill runs, so they run without a prompt. It does not restrict the tool set. The docs say "every tool remains callable"
the body the subagent model the task itself; it must be explicit, because the subagent has nothing else to go on

Two more facts from the skills docs. A forked skill runs in the background by default; set background: false when the session should wait for it. And the docs say context: fork "only makes sense for skills with explicit instructions". A skill that only says "follow our style guide" gives the subagent nothing to do. The same frontmatter applies when you invoke the skill as a slash command (/trace-status); Foundations explains how skills and slash commands relate in Are slash commands and skills different things?.

Situation Evidence Choose Why the tempting alternative is wrong
A search or audit procedure reads many files and returns a short answer long, noisy, self-contained; output is a summary context: fork (often with agent: Explore) running it inline fills the main context with search output that the session no longer needs
A skill rewrites "the function we just discussed" it needs the current conversation run inline (no context: fork) a forked subagent cannot see the conversation, so it does not know which function you mean
A forked skill must not edit files the need is a restriction agent: set to a custom agent in .claude/agents/ whose tools: line lists only read tools allowed-tools only pre-approves; it never removes a tool
A skill holds conventions, not a task guidance, no explicit steps inline skill or .claude/rules/ a forked run of pure guidance has no task to do

The MCP server allowlist β€” connections the team may use

Who writes it: the platform team. Security approves each server. Where it lives: .mcp.json in the repository holds the project-scope servers. For enforcement, the managed layer can limit which servers users may configure at all. Every [MCP server](term:MCP server) is a network connection and a set of tools. An allowlist is a removal control. It stops a developer from connecting an unreviewed server to a session that can read the codebase.

JSON
{
  "mcpServers": {
    "jira": {
      "type": "http",
      "url": "https://mcp.meridian.internal/jira"
    },
    "shipment-db-readonly": {
      "command": "npx",
      "args": ["-y", "@meridian/mcp-shipments", "--readonly"]
    }
  }
}

The shared file names two servers that the team reviewed. The --readonly argument is important. It is the least-privilege decision, made at configuration time and written in the file, not in a prompt. To enforce the list β€” so a developer cannot add a third server on their own machine β€” you need a managed-layer setting. Check the current managed-settings documentation for the exact key, because this mechanism lives above the project file.

Decision table β€” which layer owns which rule

The rule Layer and file Who changes it Wrong layer and its failure
Secrets are unreadable [managed policy](term:managed policy) security via MDM in project settings.json: any developer can delete the line
No --dangerously-skip-permissions managed policy security in CLAUDE.md: prose cannot disable a flag
Run tests before "done" root CLAUDE.md team via PR in managed policy: an instruction is not a permission; the layer cannot express it
Allow npm test without a prompt project .claude/settings.json permissions.allow team via PR in each user file: forty inconsistent grants
Which MCP servers exist .mcp.json plus managed restriction platform + security only in .mcp.json: a local override adds an unreviewed server
Editor keymap, theme user ~/.claude/settings.json the developer in project settings: a preference war in code review
Release procedure .claude/skills/release-check/SKILL.md the release owner in CLAUDE.md: loaded every session, followed inconsistently
Path-specific style .claude/rules/ with globs the folder's owners in root CLAUDE.md: every session pays for rules it never needs

The wrong choice, and what it produces. The most common team failure is policy in the wrong layer. Someone writes a security rule into the committed .claude/settings.json because "that's where permissions go". It works until the first developer is blocked by a prompt and removes the line. Nothing stops them, because developers can edit the project layer. On the exam, this wrong option (distractor) looks like a reasonable control ("add the deny rule to the shared project settings"). It fails because of ownership, not syntax. The second failure is the opposite: conventions placed in managed policy. Managed policy cannot express "run tests first" at all, and every style change must then wait for IT. The third failure is parallel work without [worktrees](term:worktree isolation): forty developers running agents in the same checkout. That is a workflow problem, not a settings problem, so it belongs to the next objective.

Which mechanism? CLAUDE.md, rules, skills, hooks, or permissions

The table above asks "who owns the rule". This one asks a second question: what kind of guidance is it, and when must it apply? Claude Code (the CLI harness) gives a team five places to put a rule. Two of them are context: text the model reads and usually follows. Three of them are executed or checked by the harness: code that runs whatever the model decides. The memory docs say it directly about CLAUDE.md: "Claude treats them as context, not enforced configuration. To block an action regardless of what Claude decides, use a PreToolUse hook instead."

Need Mechanism Where it lives (exact path) Guidance or enforced Loads when
Project conventions for every session CLAUDE.md CLAUDE.md at the repo root (or .claude/CLAUDE.md) guidance: the model reads it at launch, with its @imports; nested folder files load when work enters that folder
Conventions for one file type or folder path-scoped rule .claude/rules/handlers.md with paths: globs in its frontmatter guidance only when Claude works with files that match the globs (a rule file with no paths: loads every session)
A repeatable multi-step procedure, run on demand skill .claude/skills/release-check/SKILL.md guidance (a procedure) its description is always listed; the full body loads only when someone types /release-check or Claude matches the description
An action that must ALWAYS or NEVER happen hook, for example PreToolUse "hooks" in .claude/settings.json enforced: the harness runs the command on its event, every time ("the trigger is guaranteed", features overview)
Which tools and commands may run, with or without a prompt settings permissions "permissions" in .claude/settings.json for the team; the managed file for rules nobody may loosen enforced: the harness checks every tool call at launch; applied before each tool call

Recall line: context (CLAUDE.md, rules, skills) tells the model what to do; the harness (hooks, permissions) decides what is allowed to happen. The features overview says: "If a rule must hold every time, make it a hook rather than a prompt instruction."

πŸ“„ Project Β· CLAUDE.md (repo root) Β· example

πŸ“– Guidance: the model reads it. Claude Code loads the text into context at launch. Nothing checks that the model follows it.

With the other levels: memory files add up. The user file, the root project file, and nested folder files are all loaded, not replaced.

If they contradict: the model sees both lines and has to choose. No program resolves it, so remove the conflict. Anything that must win goes into a hook or a permission rule.

The team writes this file and reviews it by pull request. Claude Code reads it at the start of every session in tracking-api. The docs recommend keeping it under 200 lines.

MARKDOWN
# Meridian tracking-api β€” team conventions

- Run `npm test` and `npm run lint` before reporting a task complete. Paste the summary line.
- Never edit files under `migrations/`; propose a new migration file instead. (A PreToolUse hook also blocks this.)
- API handlers live in `src/handlers/`; shared validation in `src/validate/` (Zod). Do not add a third place.
- Handler-specific conventions load from `.claude/rules/handlers.md` when you work in `src/handlers/`.
- Secrets come from the environment. Never write a literal key, token, or password into any file.
- When a task touches `src/billing/`, read `src/billing/CLAUDE.md` first (nested memory).
- Use the `release-check` skill for anything that mentions shipping, deploying, or go-live.
πŸ“„ Project Β· .claude/rules/handlers.md Β· example

πŸ“– Guidance: the model reads it. The paths: frontmatter is read by Claude Code to decide when to load the file. The body is guidance, like CLAUDE.md.

With the other levels: it adds to CLAUDE.md; it does not replace it. Claude Code finds every .md file under .claude/rules/, also in subfolders.

If they contradict: the model sees both texts. Keep the rule file specific and the root file general, so they do not overlap.

The owners of src/handlers/ write it. Claude Code loads it only when Claude works with a file that matches one of the globs.

MARKDOWN
---
paths:
  - "src/handlers/**/*.ts"
---

# Handler conventions

- Every handler validates its input with a schema from `src/validate/` before any other work.
- Return errors as `{ "error": { "code": string, "message": string } }` with a 4xx status; never a bare string.
- Log one line per request with `request_id`; never log the request body.
- Each handler has a test in `src/handlers/__tests__/` with the same file name.
πŸ“„ Project Β· .claude/skills/release-check/SKILL.md Β· example

πŸ”€ Mixed. The frontmatter is read by Claude Code: it lists the skill, and disable-model-invocation: true stops Claude from starting it on its own. The numbered body is guidance that the model follows when the skill runs.

With the other levels: no merging. When two levels have a skill with the same name, one whole skill wins.

If they contradict: only the winning skill loads. The steps never grant a permission: each command still goes through the permission rules in settings.

The release owner writes it. Claude Code lists its description in every session and loads the body when a developer types /release-check.

MARKDOWN
---
name: release-check
description: Run Meridian's pre-release checklist. Use when the user says ship, deploy, release, or go-live.
disable-model-invocation: true
---

1. Confirm the branch is `main` and the working tree is clean; stop and report if not.
2. Run `npm test` and `npm run lint`; on any failure, report the failing line and stop.
3. Run `node scripts/check-migrations.mjs`; a non-zero exit means an unreviewed migration.
4. Print the exact `git log --oneline -5` and ask the user to confirm before tagging.
πŸ“„ Project Β· .claude/settings.json (permissions + hooks) Β· example

πŸ”’ Enforced: Claude Code reads it, the model never does. The harness checks permissions before each tool call and runs each hook command on its event.

With the other levels: allow, ask and deny lists merge across all settings levels; hooks from every level all run. The managed file sits above this one.

If they contradict: a deny at any level wins over an allow. So the security denies live in the managed file, where no developer can delete them.

The team commits this file; Claude Code reads it at launch. The PreToolUse hook runs before every Edit or Write. Its script exits with code 2 when the target path is under migrations/, so the edit never happens.

JSON
{
  "permissions": {
    "allow": [
      "Bash(npm test:*)",
      "Bash(npm run lint:*)",
      "Bash(git diff:*)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/block_migrations.py"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix \"$CLAUDE_FILE_PATH\" || true"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/require_tests_ran.py"
          }
        ]
      }
    ]
  }
}

The hook script, .claude/hooks/block_migrations.py (illustrative):

PYTHON
import json
import sys

payload = json.load(sys.stdin)
file_path = payload.get("tool_input", {}).get("file_path", "")

if "/migrations/" in file_path or file_path.startswith("migrations/"):
    print("Editing migrations/ is blocked. Create a new migration file instead.", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

Four "which mechanism?" decisions, with the tempting wrong choice.

Situation Evidence Choose Why the tempting alternative is wrong
CLAUDE.md says "never edit migrations/", but Claude edited a migration file last week the rule already exists as text and still failed; it must never happen a PreToolUse hook on Edit|Write that exits 2 for that path rewrite the line in capitals: it is still context, and the docs call it "a request, not a guarantee"
The root CLAUDE.md has grown to 400 lines; most lines are handler conventions the conventions apply only to src/handlers/**/*.ts .claude/rules/handlers.md with a paths: glob a skill: it loads only when invoked or matched by its description, so a quick handler edit may never load it. Keep it in CLAUDE.md: every session pays for rules it rarely needs
An eight-step release checklist that developers run once a week multi-step, on demand, has side effects a skill with disable-model-invocation: true, run as /release-check paste it into CLAUDE.md: loaded in every session and followed loosely. A hook: a hook runs one command on an event; it does not carry a procedure with judgment steps
Every CI run and laptop stops on a prompt for npm test the need is "may this command run without asking?" permissions.allow in .claude/settings.json a CLAUDE.md line "you may run npm test": prose cannot grant a permission; the harness decides before the tool runs

Exam reflex. The words "always", "never", "ensure", or "regardless" point to a hook or a permission rule. The words "only for these files" point to .claude/rules/ with paths:. "On demand" or "a repeatable workflow" point to a skill. "Every session should know" points to CLAUDE.md.

Objective 2 Β· Improve developer workflows using AI-assisted tooling

Improve developer workflows using AI-assisted tooling

A workflow improvement is a before/after table with a measurement. It is not a list of features. The workflow-improvement method has four parts: name the workflow, state the current number, redesign the steps, and measure again. The exam tests two things: do you keep the deterministic gates where they were, and do you put the model where the waiting was? Foundations covered the tools: headless mode in Claude Code in CI/CD, and the @claude team trigger in The @claude tag. On this page you decide where to use them.

Before / after β€” Meridian's three workflows

Workflow Before (measured) After (designed) What stays deterministic
PR review first review after ~48 h; reviewer reads the whole diff CI posts Claude findings in ~10 min as advisory comments; reviewer reads findings + diff tests, lint, and the required-reviewer rule still block the merge
Onboarding a new engineer ~2 weeks to first merged PR; questions answered in chat root CLAUDE.md + .claude/rules/ + skills catalog answer "where does X live"; first PR in ~4 days code review, CODEOWNERS, the same test suite
Incident write-up post-mortem written 3 days later from memory a postmortem skill drafts the timeline from the incident channel export and runbook log fields; the on-call engineer edits the on-call engineer signs the document; the model never marks an incident resolved

Each row names the number that the team will measure again. "Faster" alone is not a row.

The CI review hook β€” advisory findings, blocking tests

Who writes it: the platform team. Where it lives: .github/workflows/claude-review.yml. The token is stored in repository secrets, and the workflow is pinned to a fixed version. The job runs headless Claude Code with a limited tool allowlist and posts a comment. It does not set the check status that blocks the merge. The test job still does that.

YAML
name: claude-review
on:
  pull_request:
    types: [opened, synchronize]
permissions:
  contents: read
  pull-requests: write
jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          prompt: >
            Review this pull request against the conventions in CLAUDE.md.
            Report correctness risks and convention violations as a checklist.
            Do not modify files. Do not approve or request changes.
          claude_args: "--max-turns 12 --allowedTools Read,Grep,Glob,Bash(git diff:*)"

Read the trust boundary (what the job is allowed to do) line by line. The workflow's own permissions block allows read on contents and write on pull requests, and nothing else. The action version is pinned. --allowedTools pre-approves reading files and viewing diffs, so the job never waits on a permission prompt. Be precise here: the Claude Code docs say --allowedTools lists "tools that execute without prompting for permission". It grants approval. It does not remove Write, Edit, or other Bash commands from the session. To remove tools, you add --tools (shown in the next section). --max-turns is the budget limit. The prompt says "do not approve". But the sentence does not enforce that. The GitHub permission scope enforces it: with contents: read, the job token cannot push, whatever the model decides. This is the same instruction-versus-enforcement difference that Foundations taught.

Developers get a matching hook on their own machines, so the same convention check runs before the PR exists. This is part of a project .claude/settings.json that the team commits:

JSON
{
  "permissions": {
    "allow": [
      "Bash(npm test:*)",
      "Bash(npm run lint:*)",
      "Bash(git diff:*)"
    ]
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npx eslint --fix \"$CLAUDE_FILE_PATH\" || true"
          }
        ]
      }
    ],
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/require_tests_ran.py"
          }
        ]
      }
    ]
  }
}

The PostToolUse hook formats every file that Claude writes. This is a side effect, not a gate. The Stop hook runs a script. The script checks whether the session's transcript contains a test run. If it does not, the script returns a message that asks Claude to run the tests before it finishes. This is the "run tests before done" convention from CLAUDE.md, but now code enforces it at the moment it matters. Text in the memory file asks; the hook checks.

PYTHON
import json
import sys

payload = json.load(sys.stdin)
transcript_path = payload.get("transcript_path", "")
ran_tests = False
try:
    with open(transcript_path, encoding="utf-8") as handle:
        for line in handle:
            if "npm test" in line or "npm run test" in line:
                ran_tests = True
                break
except OSError:
    pass

if not ran_tests:
    print("Run `npm test` and report the summary line before finishing.", file=sys.stderr)
    sys.exit(2)
sys.exit(0)

The script is illustrative. It reads the hook payload from standard input. It scans the transcript file that the payload names. If it finds no test run, it exits with code 2 and a message. A production version would check the test result, not only the command text. For the exam, the shape is what matters: a deterministic script at a lifecycle event, not a longer instruction.

A review job that loads only the standards it needs

The problem. Meridian's second repository runs the review in a plain shell step: claude -p "<prompt>". Each run starts slowly. The reason: the root CLAUDE.md has many @imports (lines like @docs/architecture.md that pull other files into memory). The memory docs say imports "load at launch". So every review loads the whole team memory, plus hooks, skills, and MCP servers, before it reads one line of the diff. But the review only needs one file: docs/review-standards.md. And a pipeline that reads the result needs JSON in a fixed shape, not prose.

The CI step (GitHub Actions run: shell, illustrative paths):

BASH
git diff origin/main...HEAD > pr.diff

claude -p "Review the change in pr.diff against the review standards. Report every finding." \
  --bare \
  --append-system-prompt-file docs/review-standards.md \
  --tools "Read,Grep,Glob" \
  --allowedTools "Read,Grep,Glob" \
  --output-format json \
  --json-schema '{"type":"object","properties":{"findings":{"type":"array","items":{"type":"object","properties":{"file":{"type":"string"},"line":{"type":"integer"},"severity":{"type":"string","enum":["blocker","major","minor"]},"rule":{"type":"string"},"message":{"type":"string"}},"required":["file","line","severity","rule","message"]}}},"required":["findings"]}' \
  --max-turns 15 \
  > review.json

jq '.structured_output.findings' review.json

Each flag, what it does, and who acts. All of these are read by Claude Code (the CLI), not by the model.

Flag What it does (from the CLI reference) Why the review needs it
-p print mode: runs once, prints the response, no interactive session CI has nobody at the terminal (headless)
--bare skips auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, and CLAUDE.md removes the slow @import load; gives "the same result on every machine"
--append-system-prompt-file <file> appends the file's text to the default system prompt adds back only the standards; Claude keeps its default coding and tool guidance
--tools "Read,Grep,Glob" restricts which built-in tools Claude can use a review reads; it has no reason to have Edit, Write, or Bash
--allowedTools "Read,Grep,Glob" tools that execute without prompting for permission nobody can answer a prompt in CI; this pre-approves, it does not restrict
--output-format json prints a JSON result object (result, session_id, total_cost_usd, …) the next step parses it; the cost rows on this page read from it
--json-schema '<schema>' validated JSON matching the schema "after the agent completes its workflow" (print mode only); the output sits in structured_output downstream code gets a fixed shape; an invalid schema is an error exit
--max-turns 15 limits agentic turns; exits with an error when the limit is reached a runaway review fails loudly instead of running up cost

Two facts to remember about --bare. First, authentication: "In bare mode, Claude Code never reads OAuth credentials or the system keychain." The job needs ANTHROPIC_API_KEY in the environment (from repository secrets), or an apiKeyHelper passed with --settings. Second, the docs do not say whether .claude/settings.json is read in bare mode. So do not rely on project permission rules here: put the tool limits on the command line, as above. The docs also do not say whether @imports inside an appended file are resolved, so keep docs/review-standards.md self-contained.

The three tempting options, side by side.

Option What happens Verdict
claude -p --bare alone fast start, but CLAUDE.md is skipped, so the standards are dropped entirely wrong: the review checks against nothing
plain claude -p loads CLAUDE.md and every import, plus hooks, MCP servers, and skills wrong: this is the slow start the team wants to remove
--bare --system-prompt-file docs/review-standards.md the file replaces the entire default prompt wrong here: the docs say replacing "drops all of the default prompt, including tool guidance and safety instructions"; use replacement only when the surface or identity is different, not for a coding review that still uses tools
--bare --append-system-prompt-file docs/review-standards.md fast start, default prompt kept, only the standards added right
🏭 Production shape: the schema in a reviewed file, and a consumer that fails closed

In production the schema is not an inline string. It is a committed file that the team reviews like code, and the consumer step checks the exit code before it trusts the output:

BASH
set -euo pipefail
git diff origin/main...HEAD > pr.diff

claude -p "Review the change in pr.diff against the review standards. Report every finding." \
  --bare \
  --append-system-prompt-file docs/review-standards.md \
  --tools "Read,Grep,Glob" \
  --allowedTools "Read,Grep,Glob" \
  --output-format json \
  --json-schema "$(cat .github/review/findings.schema.json)" \
  --max-turns 15 \
  > review.json

jq -e '.structured_output.findings | type == "array"' review.json > /dev/null
jq -c '.structured_output.findings[]' review.json | while read -r finding; do
  node .github/review/post-comment.mjs "$finding"
done
jq '{cost: .total_cost_usd, session: .session_id}' review.json >> "$GITHUB_STEP_SUMMARY"

.github/review/findings.schema.json:

JSON
{
  "type": "object",
  "properties": {
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "file": { "type": "string" },
          "line": { "type": "integer" },
          "severity": { "type": "string", "enum": ["blocker", "major", "minor"] },
          "rule": { "type": "string" },
          "message": { "type": "string" }
        },
        "required": ["file", "line", "severity", "rule", "message"]
      }
    }
  },
  "required": ["findings"]
}

set -e makes a non-zero exit from Claude Code (an invalid schema, or the --max-turns limit) fail the job instead of posting an empty review. The findings stay advisory: this job posts comments; the test job still sets the merge status.

Exploring an unfamiliar codebase β€” Glob, Grep, then Read

The onboarding row above depends on this skill. A new engineer asks Claude Code, "How does a shipment get its ETA?" The repository has 2,000 files. If Claude reads files one by one, the context window fills long before the answer appears. The fix is an incremental strategy: each step narrows the search, and only the last step reads real code.

Step Tool Question it answers Meridian example
1 Β· Map Glob which files exist with this name pattern? src/**/*eta*.ts β†’ 6 files
2 Β· Locate Grep where is this symbol defined and used? computeEta β†’ 1 definition, 3 call sites
3 Β· Understand Read (only the matches, or a line range) what does this code actually do? src/eta/computeEta.ts plus the lines around each call
4 Β· Follow Grep again, on what step 3 revealed what does it depend on? getTransitTimes β†’ src/carriers/
5 Β· Delegate (when wide) an Explore subagent where is X used across the whole repo? returns a file:line table; the main session keeps only the table
Situation Evidence Choose Why the tempting alternative is wrong
"Explain how ETA works" in a large, unknown repo many files, one question Glob β†’ Grep β†’ Read the matches Read every file in src/: fills the context window with code that does not answer the question, and leaves less room for the work that follows
"List every place that writes shipment status" a wide search; only the list matters an Explore subagent (or a context: fork skill like /trace-status) search inline: forty search results stay in the main conversation after the answer arrives
Claude reaches for cat and find through Bash the built-in tools already do this Read, Glob and Grep Bash: it runs any shell command, so it needs broader permission than a read-only review should have, and its raw output is not narrowed for you
Step 3 shows the file is 1,800 lines long only one function matters Read with a line range, after Grep gave the line number read the whole file: most of it is unrelated to the question

Foundations covers the Explore subagent in Plan mode vs direct execution β€” and the Explore subagent.

Decision table β€” where does the AI step go?

Situation in the workflow Put the model here Keep this deterministic Wrong placement and its failure
Waiting for a human to read a large diff advisory findings in CI (headless) test + lint checks block the merge model verdict sets the merge status: unexplainable blocks, reviewers stop reading
Repeated "where does X live" questions root CLAUDE.md + skills CODEOWNERS and review answer in chat every time: the knowledge never lands in the repo
Formatting after every edit nowhere β€” a PostToolUse hook runs the formatter the formatter asking the model to "always format": inconsistent, costs tokens
Multi-file refactor across two branches at once one subagent per [worktree](term:worktree isolation) the merge two agents in one checkout: interleaved edits, corrupted state
A procedure with a checklist (release, rollback) a skill with the steps the commands the steps run a paragraph in CLAUDE.md: followed loosely, loaded always

The wrong choice, and what it produces. The classic distractor is automation that removes the deterministic gate: "let Claude approve the PR when its review finds no issues". It sounds efficient, but it fails as a control. When a model finds no issues, that does not prove there are no issues. And nobody can audit why the merge was allowed. The second distractor is AI chat instead of documentation: the team answers the same onboarding questions in a channel forever, instead of writing them where every session loads them. The third is a longer instruction where a hook belongs: three paragraphs of "please always run tests" that a busy session skips, instead of a ten-line Stop hook that cannot be skipped.

Adoption metrics β€” how you know the workflow improved

Where the numbers come from. The managed policy on this page turned on Claude Code's OpenTelemetry export and sent it to the team collector. That gives you session counts, token and cost usage, and tool-decision events, and you do not have to add any measuring code yourself. The metric and event names are in the Claude Code monitoring documentation. Read them there; do not memorize them. The CI job's own logs give cost and duration per PR. GitHub gives review wait time and defect data. An adoption-metrics table combines them.

Category Metric Source Baseline (week 0) Target (week 6) Decision it drives
Usage weekly active developers OTel session events 3 of 40 β‰₯ 30 of 40 expand or pause the rollout
Usage sessions per developer per week OTel 0.4 β‰₯ 5 which teams need enablement time
Quality defect escape rate, AI-assisted PRs vs others issue tracker labels 6.1% (all PRs) no worse than baseline keep or change the review prompt
Quality revert rate within 7 days git 2.3% ≀ 2.3% same
Cost cost per merged PR (CI review job) CI job output total_cost_usd β€” within budget line adjust --max-turns, model tier, caching
Cost tokens per session, p50 / p95 OTel token usage β€” p95 flagged for review find runaway sessions
Trust CI findings marked useful by reviewers PR reaction / label β€” β‰₯ 60% rewrite the review prompt or scope
Trust permission prompts per session OTel tool-decision events 14 ≀ 4 tune permissions.allow in the project file

Every row ends in a decision. A metric with no decision column is only reporting, not enablement.

Below is one illustrative record that the collector gets for each CI run. The cost rows read from this shape. When you ask headless Claude Code for JSON output, it prints a JSON result object. The fields shown are the ones the table uses; real output has more fields:

JSON
{
  "type": "result",
  "subtype": "success",
  "session_id": "5c1f0c2e-illustrative",
  "num_turns": 7,
  "duration_ms": 412000,
  "total_cost_usd": 0.31,
  "usage": {
    "input_tokens": 48210,
    "cache_read_input_tokens": 39004,
    "output_tokens": 2110
  },
  "result": "Review complete: 2 correctness risks, 1 convention violation."
}

total_cost_usd and num_turns supply the cost rows. cache_read_input_tokens inside usage tells you whether runs reuse the repeated convention prefix. This is the same [prompt caching](term:prompt caching) signal that Foundations taught, but now you read it as a metric across all runs.

Objective 3 Β· Support debugging and operational issue resolution

Support debugging and operational issue resolution

This section debugs two different systems, and the exam expects you to tell them apart. The first is the developer tool: Claude Code on a laptop or in CI. Here the questions are "why was this command blocked", "why did the hook not fire", and "why did the MCP server not load". The second is a Claude-powered production service: an application your team built on the Messages API or the [Agent SDK](term:Agent SDK). Here the questions are "why did quality drop", "why are we seeing refusals", and "why did latency double". AI-assisted debugging covers both, but the tools for each are different. The developer tool has built-in diagnostics. The production service has only what you logged.

Debugging the developer tool β€” what is built in

Symptom First check Where the answer lives
"Claude won't run my command" which layer denied it /permissions shows the merged rules; /status shows which settings files loaded
"My hook never fires" the matcher and the event /hooks lists registered hooks; run with --debug to see hook invocations
"The MCP server is missing" scope and startup /mcp shows server status; claude mcp list shows what is configured at each scope
"It ignored CLAUDE.md" which files loaded /memory shows the loaded memory files; nested memory loads only when work enters the folder
"It's slow / expensive today" context size and turns /cost for the session; OTel token metrics for the fleet

The pattern is the same as settings precedence: before you change anything, find which layer made the decision. Suppose a developer "fixes" a blocked command by adding Bash(*) to their user settings. They have not found the layer that denied it. If that layer was managed policy, the fix does nothing.

Debugging the production service β€” what you must have logged

Who decides: the architect, at design time. Where it lives: the application writes one structured log line for every model call and every tool run. If a field is not in the log, the on-call engineer cannot ask about it later. This is the minimum record, shown as an illustrative log line from the ETA assistant:

JSON
{
  "ts": "2026-09-16T02:14:07Z",
  "service": "eta-assistant",
  "run_id": "run_8f31",
  "request_id": "req_illustrative_01",
  "model": "YOUR_SUPPORTED_MODEL",
  "prompt_version": "eta-v14",
  "index_version": "shipments-2026-09-15",
  "stop_reason": "refusal",
  "usage": {
    "input_tokens": 6120,
    "cache_read_input_tokens": 0,
    "output_tokens": 0
  },
  "tool_calls": [
    {
      "id": "toolu_illustrative",
      "name": "get_shipment",
      "is_error": false,
      "latency_ms": 210
    }
  ],
  "latency_ms": 1840,
  "http_status": 200
}
Field Why the runbook needs it
request_id the id the provider can match to its own records; include it in any support case
model and prompt_version "did we change something?" β€” the first triage question
index_version for systems that use retrieval, the second triage question
[stop_reason](term:stop reason) refusal, max_tokens, end_turn, tool_use each point to a different cause
usage.cache_read_input_tokens a sudden drop to zero means the cached prefix changed β€” often this is the real "deploy"
tool_calls[].is_error and latency tells "the model is wrong" apart from "the tool is down"
http_status 429 and 529 are [rate limiting](term:rate limiting) or overload, not a quality problem

The incident triage flow β€” cause before symptom

An incident triage flow for a Claude-powered system puts the checks in order. It starts with what changed most recently and what is cheapest to check. Here it is as the runbook's decision path:

TEXT
ALERT: quality / refusal / latency alarm on a Claude-powered service

1. Is it the provider?
   β†’ http_status 429/529, error rate across ALL services, provider status page
   β†’ yes: back off, degrade gracefully, wait; do NOT change prompts
2. Did WE change something in the last window?
   β†’ prompt_version, model, index_version, tool schema, deploy timestamp
   β†’ yes: roll back that one change; confirm the metric recovers; then investigate
3. Is the input different?
   β†’ input_tokens p95 jump, new document type, new customer, injection attempt
   β†’ yes: sample 20 requests; classify; add a bound or a detector upstream
4. Is a tool failing?
   β†’ tool_calls[].is_error rate, tool latency, downstream dependency health
   β†’ yes: fix or disable the tool; the model was reporting the failure, not causing it
5. Only now: is the model behaving differently on the same input?
   β†’ replay the golden set against the current configuration
   β†’ regression: escalate with request_ids; compare against the last passing eval

The order of the steps is the whole lesson. A team that starts at step 5 rewrites prompts. Meanwhile the real cause β€” an index refresh at 01:50 β€” sits in a log field that nobody checked. The exam guide's own sample question for Domain 4 tests exactly this reflex: when answers are confident but wrong after a document refresh, look at retrieval, not the model.

The runbook template β€” written before the incident

Who writes it: the architect writes the first draft. After the first incident, the on-call team owns it. Where it lives: in the repository, next to the service, with the same version history. An operational runbook always has the same headings. So under stress, the engineer finds the right section quickly:

MARKDOWN
# Runbook Β· eta-assistant

### Symptoms this runbook covers
- Refusal rate > 2% over 10 min Β· p95 latency > 4 s Β· golden-set pass rate < 95%

### Collect first (copy-paste)
- `logs query service=eta-assistant last=15m fields=request_id,stop_reason,model,prompt_version,index_version,http_status`
- `deploys list service=eta-assistant last=2h`
- `index versions shipments last=3`

### Triage order
1. Provider (status page, 429/529 share) β†’ 2. Our last change β†’ 3. Input shift β†’ 4. Tool errors β†’ 5. Model regression (golden set replay)

### Rollback
- Prompt: `config set eta-assistant prompt_version=<previous>` (takes effect on next request; no deploy)
- Index: `index alias shipments --to <previous version>`
- Model tier: `config set eta-assistant model=<previous supported model>`

### Escalate when
- Provider incident longer than 30 min β†’ status update to stakeholders every 30 min
- Golden-set regression with no config change β†’ open a provider support case with 5 request_ids

### After
- Post-mortem within 2 business days using the `postmortem` skill; add the new failure to this runbook

Most teams forget to write the rollback section, and it is the section they need most at 2 AM. Every rollback here is a configuration change, not a code deploy. That works because the prompt version, index alias, and model tier were designed as runtime configuration. That decision was made in Domain 5's reliability engineering, and here it pays off.

Decision table β€” control types in operational support

You need to… Right control Type Wrong control and its failure
Know what happened at 2 AM structured log per call with request_id, stop_reason, versions detective ask the model to "explain what went wrong": it has no memory of the call
Stop the damage fast rollback of the one changed version corrective / removal quick-fix the prompt in production: a second uncontrolled change
Prove it was not a regression [golden set](term:golden set) replay against current config detective / eval compare two chat transcripts by eye: not repeatable
Prevent the next index refresh from breaking prod eval gate on index publish preventive alert on quality drop after publish: detective, catches it late
Keep retries from double-charging idempotency key on side-effecting tools preventive "retry carefully" in the prompt: the model does not run the retry
Handle provider overload backoff + graceful degradation compensating rewrite prompt during a 529 wave: unrelated to the cause

The wrong choice, and what it produces. The most common distractor is symptom over cause: "adjust the prompt to reduce refusals", while the log shows that index_version changed an hour before the alarm. The second is a detective control when the question asks for a preventive one: "add an alert on quality drop", when the question asked how to stop bad indexes from reaching production. The alert catches the problem only after users saw it. The third is asking the model to do the operator's job: "have Claude summarize the logs and decide whether to roll back". A summary is useful. But the decision does not belong to the model, and the runbook exists so that the decision is already made.

The enablement plan β€” the artifact an architect hands to a team

The enablement plan brings the three objectives together on one page. It is a markdown document in the platform team's repository. The engineering manager and security review it, and the team updates it at each stage gate:

MARKDOWN
# Enablement plan Β· Claude Code at Meridian Freight

**Owner:** platform team lead Β· **Sponsor:** CTO Β· **Security reviewer:** AppSec lead
**Scope:** 40 developers, 6 repositories, 6 weeks Β· **Out of scope:** production Claude services (separate runbook)

### Goal and measures
| Goal | Measure | Baseline | Target |
|---|---|---|---|
| Faster PR review | time to first review | 48 h | ≀ 4 h |
| No quality regression | defect escape rate | 6.1% | ≀ 6.1% |
| Controlled cost | cost per merged PR | β€” | within budget line |
| Safe by default | secrets readable by the tool | unknown | 0 (managed deny) |

### Stages
| Week | Stage | Deliverable | Exit check |
|---|---|---|---|
| 0 | Baseline | metrics captured | four baselines recorded |
| 1 | Pilot | managed policy on 3 machines | `Read(.env)` refused on all 3 |
| 2 | Conventions | root CLAUDE.md, project settings, skills catalog merged | PR approved by team + security |
| 3 | Workflow | CI review job on `tracking-api` | findings posted on 10 PRs; merge gate unchanged |
| 4 | Broad | managed policy + telemetry on 40 machines | β‰₯ 35 sessions/week in collector |
| 6 | Review | metrics review, go/no-go on 5 more repos | targets met or plan revised |

### Support
- Office hours: Tue/Thu 30 min Β· channel: #claude-enablement Β· owner responds within 1 business day
- Debugging the tool: see `docs/claude-code-troubleshooting.md` (`/status`, `/permissions`, `/hooks`, `/mcp`)
- Policy change requests: PR to `platform/managed-settings/` reviewed by AppSec

### Risks and controls
| Risk | Control | Type |
|---|---|---|
| Developer loosens security rule | rule lives in managed policy | preventive |
| Model verdict blocks merges unexplainably | findings advisory; tests block | preventive |
| Runaway session cost | `--max-turns`, p95 token alert | preventive + detective |
| Stale skill followed | catalog `last verified` column, quarterly review | detective |

You have already seen every table in the plan on this page. The measures are the adoption metrics. The stages are the managed rollout. The risks are the control-type comparisons. This is on purpose: the plan is this page, made short enough for a manager to sign.

See it run β€” one team's rollout, week 0 to week 6

πŸ” See it run β€” Meridian Freight from baseline to the first incident, with the real artifacts at each step

Week 0 β€” baseline. Before anything is installed, the platform lead collects four numbers: 48 h to first review (from GitHub), 6.1% defect escape (from the issue tracker), no cost line yet, and secrets exposure unknown. These numbers go into the Goal table of the enablement plan. Nothing else happens this week. Without a baseline, a rollout cannot later prove any improvement.

Week 1 β€” pilot the policy. IT deploys the managed file from Objective 1 to three volunteers. Each volunteer opens Claude Code and runs the exit check:

BASH
claude -p "Read the file .env and print its contents" --output-format json

The result JSON shows that the read was refused, and no file content appears in result. One pilot's session also shows "permission_mode": "default" in the /status output. This confirms that bypass mode is disabled. The pilot exit check passes on all three machines, and the plan records that the stage gate passed.

Week 2 β€” conventions by pull request. A PR adds four things: the root CLAUDE.md; the project .claude/settings.json with the allow list and the two hooks; .mcp.json with the two reviewed servers; and .claude/skills/release-check/SKILL.md. AppSec reviews the settings file and confirms it has no deny rules (those belong in managed policy). That reviewer comment becomes a line in the team's review checklist. The PR merges.

Week 3 β€” the CI review job on one repository. The workflow from Objective 2 goes live on tracking-api. The first ten PRs get findings comments. One finding is wrong: it flags a pattern in src/validate/ that the team uses on purpose. The reviewer marks it not useful. That reaction is the first data point in the Trust row. The team adds one sentence to the prompt that points to the nested src/validate/CLAUDE.md, and the false positive stops. Nobody changed the merge gate.

Week 4 β€” broad rollout with telemetry. The managed file, now with the telemetry env block, reaches all forty machines. By Friday the collector shows 37 distinct sessions. On average there are 14 permission prompts per session. The team reads the tool-decision events and finds that npm run build and git diff cause prompts all the time. They add both to the project allow list by PR. The next week's average is 3.

Week 5 β€” the first incident (in the production service, not the tool). At 02:11 the ETA assistant's refusal alarm fires. The on-call engineer opens the runbook and runs the "collect first" queries. The log lines show stop_reason: "refusal" on 4% of calls. prompt_version: "eta-v14" has not changed, model has not changed, and http_status: 200 is normal. So it is not the provider, and it is not a prompt change. index_version reads shipments-2026-09-15, published at 01:50. Step 2 of the triage flow says: roll back that one change.

BASH
index alias shipments --to shipments-2026-09-14

Refusals go back to the baseline level within five minutes. The engineer then looks at twenty of the refused requests. The new index had loaded a batch of customer emails that contained hidden instructions. The model refused to follow them. The behavior was correct; the input was wrong. The post-mortem action items are an eval gate on index publish (preventive) and an injection check on the ingestion path. Both become runbook lines and a row in the plan's Risks table.

Week 6 β€” review. Time to first review: 3.5 h. Defect escape rate: 5.8%. Cost per merged PR: within the budget line, and cache_read_input_tokens confirms that runs reuse the convention prefix. CI findings marked useful: 71%. Weekly active developers: 36 of 40. The plan's go/no-go decision is "go", so five more repositories enter the Week 2 stage. The team updates the enablement plan; it does not rewrite it.

Exam lens β€” how CCAR-P asks about this domain

In the question text (the stem), find the actor and the layer before you read the options. Domain 7 stems are short. The word that decides the answer is usually one of these:

Words the answer key uses in its explanations. Preventive (managed deny, allowlist, eval gate before publish) wins over detective (alert, log, audit) when the stem asks you to stop something from happening. Removal (take the capability or server out of the configuration) wins over compensating (confirmation prompt, monitoring) when the stem says least privilege. Cause (the changed version in the log) wins over symptom (the refusal rate) when the stem says "first". And enforcement (hook, permission, policy) wins over instruction (text in CLAUDE.md, a sentence in a prompt) whenever the stem says "ensure" or "guarantee".