🎓 Claude Architect KB CCAR-F · CCAR-P study guide

Domain 2 · Claude Code Configuration & Workflows (20%)

This domain explains three things about Claude Code. Where do its instructions and settings live? When does Claude Code load them? Which mechanisms run an action, and which ones block it?

📖 New here? From "my terminal assistant" to "the team's platform"

When you work alone, Claude Code works without setup. You chat, and it writes code. The problems in this domain start on the day a second person joins. That person does not know the build commands you taught Claude. The code Claude writes for them does not follow your formatting. And they let Claude run a command that you would never allow.

The solution is configuration stored as files in git: Each file has one job. A CLAUDE.md file lets the repo teach your conventions to every session (this is guidance, not enforcement). Slash commands mean nobody types the same 20-line prompt again. Skills keep a procedure ("how we do releases") in one place. Hooks run formatting and validation checks. Managed permissions limit what the agent may do. CI checks and limited credentials test changes without giving the test runner write access to the repository. See where each control lives and what it enforces. With these files, Claude Code stops being a personal assistant. It becomes a team platform with rules.

Why the SDK matters: Once you trust that platform, you will want the same engine inside your own product (a support bot, a pipeline worker). The Claude Agent SDK is Claude Code's engine packaged as a library. It has the same loop, tools, hooks and permissions. It runs without a terminal UI, and your code embeds it. So there is one ladder: terminal user → team platform → product engine. This domain covers the middle step.

What this means for the exam: Most questions here ask "which mechanism?". The choices are memory (CLAUDE.md), skill, slash command, hook, subagent, or setting. The comparison table below answers all of them.

How this page is organized: the six exam tasks

Domain 2 is simple at the top: how you configure Claude Code, and how you work with it. The official exam guide splits that into six tasks. This page follows them in order. Every section opens with a box that says what it teaches, the scenario, and which exam task it serves.

Numbering note: this KB calls it "Domain 2". The official exam guide calls the same content Domain 3: Claude Code Configuration & Workflows (20%), so its tasks are numbered 3.1–3.6.

Part Exam task What you must be able to decide Start here
1 3.1 CLAUDE.md hierarchy and files Which file and which level an instruction or setting belongs in, and whether it is guidance or enforced The file map · Rule or guidance?
2 3.2 Slash commands and skills Command vs skill vs CLAUDE.md; project vs personal; context: fork, allowed-tools, argument-hint Skills vs commands vs subagents
3 3.3 Path-specific rules .claude/rules/ with paths: globs vs a subfolder CLAUDE.md .claude/rules/
4 3.4 Plan mode vs direct execution Plan first, just do it, or both; when to use the Explore subagent Plan mode vs direct execution
5 3.5 Iterative refinement Examples, tests first, the interview pattern, one message or several Iterative refinement
6 3.6 CI/CD -p, --output-format json + --json-schema, --bare + --append-system-prompt-file, CLAUDE.md in CI, a separate review session Claude Code in CI/CD
7 Other domains Hooks (official 1.5), which tools an agent gets (2.3), built-in tools (2.5), the Agent SDK Hooks

The Claude Code file map: every file, where it lives, who wins

Here's the trick. Claude Code reads config from four levels: Organization (your IT team), User (you, every project), Project (the team, committed in git) and Local (you, this one project). At each level there are two very different kinds of file. Settings files (*.json) are rules that Claude Code itself enforces: permissions, hooks, env, model. Instruction files (CLAUDE.md, .claude/rules/*.md) are advice that the model reads; nothing enforces them. (Rule or guidance? The full explanation.) Settings fight each other by precedence, and the higher level wins. Instruction files never fight: Claude Code loads all of them, one after the other.

The map

What Organization (managed) User (you, every project) Project (team, in git) Local (you, this project)
Settings (permissions, hooks, env, model…) ‹managed›/managed-settings.json + optional ‹managed›/managed-settings.d/*.json ~/.claude/settings.json .claude/settings.json .claude/settings.local.json
Instructions (CLAUDE.md) ‹managed›/CLAUDE.md, or the claudeMd key inside managed-settings.json ~/.claude/CLAUDE.md ./CLAUDE.md or ./.claude/CLAUDE.md ./CLAUDE.local.md (add it to .gitignore yourself)
Path rules not documented ~/.claude/rules/*.md .claude/rules/*.md not documented
Subagents ‹managed›/.claude/agents/*.md ~/.claude/agents/*.md .claude/agents/*.md —
Skills (and old commands) ‹managed›/.claude/skills/<name>/SKILL.md ~/.claude/skills/<name>/SKILL.md (old style: ~/.claude/commands/*.md) .claude/skills/<name>/SKILL.md (old style: .claude/commands/*.md) —
MCP servers ‹managed›/managed-mcp.json (exclusive: only these servers load), or the managedMcpServers key in managed settings (added on top of the user's own) ~/.claude.json (top level, "user scope") .mcp.json at the project root (not inside .claude/) ~/.claude.json, under this project's path ("local scope")
Hooks: where registered managed-settings.json → hooks ~/.claude/settings.json → hooks .claude/settings.json → hooks .claude/settings.local.json → hooks
Hooks: where the script lives wherever the command points (path not fixed by the docs), e.g. /usr/local/bin/acme-audit-bash.sh wherever the command points, e.g. ~/.claude/hooks/check-bash.sh wherever the command points, e.g. .claude/hooks/format.sh wherever the command points

‹managed› is the system folder for organization files. macOS: /Library/Application Support/ClaudeCode/. Linux and WSL: /etc/claude-code/. Windows: C:\Program Files\ClaudeCode\ (Claude Code does not read the old C:\ProgramData\ClaudeCode\ path). Managed settings can also arrive without a file: from the claude.ai admin console, an MDM profile (macOS domain com.anthropic.claudecode), or the Windows registry (HKLM\SOFTWARE\Policies\ClaudeCode). Click any path with ↗ to see a full example file. All the examples belong to one developer on one repo, acme-app, so they fit together.

What is managed-settings.d/? It is an optional folder next to managed-settings.json for splitting the org policy into several files. Why: in a big company, different teams own different parts of the policy. The telemetry team owns telemetry, security owns the deny rules. Instead of everyone editing one shared file, each team ships its own file, for example 10-telemetry.json and 20-security.json. Claude Code reads managed-settings.json first, then every *.json in the folder in alphabetical order (that's why the number prefixes), and merges them into one org policy. Inside that merge: single values → the later file wins; lists like permissions.deny → combined, duplicates removed; blocks like env → merged key by key. It ignores hidden files and files that don't end in .json. A file that isn't valid JSON stops Claude Code from starting. To you as a developer it all looks like one managed level. Official: split a policy across teams↗

Hooks can also be registered in a plugin's hooks/hooks.json, in a skill's frontmatter, and in a subagent's frontmatter. ~/.claude.json is a separate file that Claude Code writes for itself (sign-in, trust decisions, your MCP servers). It is not ~/.claude/settings.json.

Who wins?

Settings: the higher level wins. This is the official order, highest first:

  1. Managed settings: managed-settings.json, MDM, or the claude.ai console. Nothing in your own files or in --settings overrides a managed key.
  2. Command line: claude --settings '<json or file>', for this session only.
  3. Project local: .claude/settings.local.json.
  4. Shared project: .claude/settings.json.
  5. User: ~/.claude/settings.json.

So yes, your personal ~/.claude/settings.json is the lowest. The team file beats it. Your local file beats the team file.

Single values: the higher level wins. Lists: they merge. model set in two files? Claude Code takes the value from the higher one. permissions.allow set in three files? Claude Code combines all three lists. env is merged per variable: when two files set the same variable, the higher one applies. hooks merge across files too, and nobody can remove a managed hook from another file.

Deny beats allow, at any level. Claude Code checks rules in this order: deny, then ask, then allow. The first match decides. A deny in your user file blocks an allow in the project file, and the other way round. An allow can never cut an exception out of a deny: deny: Bash(aws *) blocks allow: Bash(aws s3 ls).

A few exceptions (official list, short version):

Instruction files: no winner, all of them load. Claude Code concatenates (joins) every CLAUDE.md it finds. They don't override each other. Load order is from broadest to most specific: managed, then user, then project, then local. Inside the directory tree, it goes from the filesystem root down to your working folder, and in each folder CLAUDE.local.md comes after CLAUDE.md. So the most specific file is read last. If two instructions contradict each other, the docs say Claude "may pick one arbitrarily". User rules (~/.claude/rules/) load before project rules, and neither overrides the other. You can skip unwanted files with claudeMdExcludes, but the managed CLAUDE.md cannot be excluded.

Careful: other file types have their own order.

The four settings files, side by side

These four files belong to one developer on one repo, acme-app. They use the same keys, so you can follow each conflict.

📄 Organization level · /Library/Application Support/ClaudeCode/managed-settings.json · example

🔒 Enforced: Claude Code (the program) reads it. These keys are not advice to the model. Claude Code applies them in code: it checks permissions before every tool call and runs hooks at their events, whatever Claude decides.

With the other levels: single values (model, cleanupPeriodDays) → the higher level wins: organization > command line > local > project > user. Lists (permissions.allow, permissions.deny) → combined. env → merged per variable. hooks → all of them run. A key set here cannot be overridden by any file below (a few security keys accept an even stricter lower value).

If two levels contradict: code always gives one answer. A deny beats an allow at any level. For a single value, the higher level wins. See the trace table.

Your IT team writes this file and pushes it to every Mac with their normal tools (MDM, Ansible…); you can't edit it, and /status shows it as Enterprise managed settings (file).

JSON
{
  "availableModels": ["claude-sonnet-5", "claude-opus-5-5"],
  "cleanupPeriodDays": 30,
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./secrets/**)",
      "Bash(aws *)"
    ],
    "disableBypassPermissionsMode": "disable"
  },
  "env": {
    "DISABLE_TELEMETRY": "1"
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "/usr/local/bin/acme-audit-bash.sh" }
        ]
      }
    ]
  },
  "claudeMd": "Never commit secrets. Follow the ACME security standard."
}

claudeMd is managed-only. The hook script path is just an example; the docs don't fix a folder for it. The org could also add managed-only locks like "allowManagedPermissionRulesOnly": true or "allowManagedHooksOnly": true, but then your own allow rules or hooks would be ignored, so this example leaves them out.

📄 User level · ~/.claude/settings.json · example

🔒 Enforced: Claude Code (the program) reads it. These keys are not advice to the model. Claude Code applies them in code: it checks permissions before every tool call and runs hooks at their events, whatever Claude decides.

With the other levels: single values (model, cleanupPeriodDays) → the higher level wins: organization > command line > local > project > user. Lists (permissions.allow, permissions.deny) → combined. env → merged per variable. hooks → all of them run. Your user file is the lowest level, so project and local files beat it for single values.

If two levels contradict: code always gives one answer. A deny beats an allow at any level. For a single value, the higher level wins. See the trace table.

You write this one (or /config and /model write it for you); it applies to every project on your machine.

JSON
{
  "$schema": "https://json.schemastore.org/claude-code-settings.json",
  "model": "claude-opus-5-5",
  "cleanupPeriodDays": 90,
  "permissions": {
    "allow": [
      "Bash(git status)",
      "Bash(git diff *)"
    ]
  },
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/check-bash.sh" }
        ]
      }
    ]
  }
}
📄 Project level · acme-app/.claude/settings.json · example

🔒 Enforced: Claude Code (the program) reads it. These keys are not advice to the model. Claude Code applies them in code: it checks permissions before every tool call and runs hooks at their events, whatever Claude decides.

With the other levels: single values (model, cleanupPeriodDays) → the higher level wins: organization > command line > local > project > user. Lists (permissions.allow, permissions.deny) → combined. env → merged per variable. hooks → all of them run. The team file beats your user file. Your local file beats it.

If two levels contradict: code always gives one answer. A deny beats an allow at any level. For a single value, the higher level wins. See the trace table.

The team writes this and commits it to git; everyone who clones acme-app gets it (allow rules and most env values wait until each person trusts the folder).

JSON
{
  "model": "claude-sonnet-5",
  "permissions": {
    "allow": [
      "Bash(mvn test *)",
      "Bash(mvn package *)"
    ],
    "deny": [
      "Bash(rm -rf *)"
    ]
  },
  "env": {
    "MAVEN_OPTS": "-Xmx2g"
  },
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh" }
        ]
      }
    ]
  }
}
📄 Local level · acme-app/.claude/settings.local.json · example

🔒 Enforced: Claude Code (the program) reads it. These keys are not advice to the model. Claude Code applies them in code: it checks permissions before every tool call and runs hooks at their events, whatever Claude decides.

With the other levels: single values (model, cleanupPeriodDays) → the higher level wins: organization > command line > local > project > user. Lists (permissions.allow, permissions.deny) → combined. env → merged per variable. hooks → all of them run. Your local file beats the project and user files, but never the organization file or a deny rule.

If two levels contradict: code always gives one answer. A deny beats an allow at any level. For a single value, the higher level wins. See the trace table.

Only you have this file; Claude Code creates it when you answer "Yes, and don't ask again" on a prompt and keeps it out of git for you.

JSON
{
  "model": "claude-opus-5-5",
  "permissions": {
    "allow": [
      "Bash(docker compose up *)",
      "Bash(aws s3 ls)"
    ]
  },
  "env": {
    "MAVEN_OPTS": "-Xmx4g"
  }
}

Trace it: what Claude Code actually ends up with

Key Org User Project Local Final value Why
model — claude-opus-5-5 claude-sonnet-5 claude-opus-5-5 claude-opus-5-5 Single value: highest level that sets it is Local. It is inside the org's availableModels, so it's allowed
availableModels sonnet-5, opus-5-5 — — — the org list, as-is Managed list applies as-is; lower additions ignored
cleanupPeriodDays 30 90 — — 30 Single value: managed beats everything
permissions.allow — git status, git diff mvn test, mvn package docker compose up, aws s3 ls all six combined Lists merge
permissions.deny .env, secrets, aws * — rm -rf * — all four combined Lists merge
Bash(aws s3 ls) deny aws * — — allow blocked Deny is checked before allow, at any level
disableBypassPermissionsMode "disable" — — — "disable" Only one level sets it
env.MAVEN_OPTS — — -Xmx2g -Xmx4g -Xmx4g Same variable: higher level (Local) wins
env.DISABLE_TELEMETRY 1 — — — 1 env merges per variable, so this one stays
hooks PreToolUse audit PreToolUse check-bash PostToolUse format — all three run Hooks merge across files
claudeMd text — — — loaded as managed memory Managed-only key

Instruction, agent, skill and MCP files: examples

📄 Organization level · /Library/Application Support/ClaudeCode/CLAUDE.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: nothing overrides anything. Claude Code loads all CLAUDE.md files together, in this order: organization → user → project → local, plus a subfolder's CLAUDE.md when Claude works in that folder. The more specific file is read last. This organization file loads first and cannot be excluded.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

IT writes this and deploys it with MDM, Group Policy or Ansible; it loads first in every session and nobody can exclude it.

MARKDOWN
# ACME engineering standards

- Never put secrets, tokens or customer data in code, logs or commits.
- All new services use Java 21 and Spring Boot 3.
- Ask before changing anything under `infra/` or `.github/workflows/`.
📄 User level · ~/.claude/CLAUDE.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: nothing overrides anything. Claude Code loads all CLAUDE.md files together, in this order: organization → user → project → local, plus a subfolder's CLAUDE.md when Claude works in that folder. The more specific file is read last. Yours loads after the organization file and before the project file.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

You write this; it loads in every project, after the org file.

MARKDOWN
# My preferences

- Explain in plain, short sentences.
- Prefer constructor injection over field injection.
- Run the tests before you say a task is done.
📄 Project level · acme-app/CLAUDE.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: nothing overrides anything. Claude Code loads all CLAUDE.md files together, in this order: organization → user → project → local, plus a subfolder's CLAUDE.md when Claude works in that folder. The more specific file is read last. The team file loads after yours, so Claude reads it later.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

The team commits this; it loads after the user file, so Claude reads it later.

MARKDOWN
# acme-app

- Build: `mvn package`. Test: `mvn test`.
- Layout: `src/main/java/com/acme/api` = REST controllers, `.../service` = business logic.
- Every controller method validates its input with Bean Validation.
- Error responses always use `ErrorResponse { code, message }`.
📄 Project path rule · acme-app/.claude/rules/api.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: a rule file loads like a CLAUDE.md. With paths: it loads only when Claude touches a matching file. User rules load before project rules, and neither overrides the other.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

The team commits this; because of paths:, it loads only when Claude reads, writes or edits a matching file.

MARKDOWN
---
paths:
  - "src/main/java/com/acme/api/**/*.java"
---

# API rules

- Every endpoint has an OpenAPI annotation.
- Never return a JPA entity; map it to a DTO.
- Use the standard `ErrorResponse` format.
📄 Project subagent · acme-app/.claude/agents/code-reviewer.md · example

🔀 Mixed. The body below the --- becomes the subagent's system prompt: guidance the model reads. The tools: line is enforced: Claude Code gives the subagent only those tools.

With the other levels: no merging. When two levels define the same name, one whole file wins: organization > --agents flag > project > user > plugin. Subagents with different names are all available.

If they contradict: the losing file is not read at all, so its lines can't conflict with the winner.

The team commits this; Claude delegates to it when the task matches description, or you type @ and pick it. The body becomes the subagent's system prompt.

MARKDOWN
---
name: code-reviewer
description: Reviews code for correctness, security, and maintainability
tools: Read, Grep, Glob
---

You are a senior code reviewer. Review for:

1. Correctness: logic errors, edge cases, null handling
2. Security: injection, auth bypass, data exposure
3. Maintainability: naming, complexity, duplication
📄 Project skill · acme-app/.claude/skills/security-review/SKILL.md · example

🔀 Mixed. The body is instructions Claude reads when the skill runs: guidance. The frontmatter (for example disable-model-invocation) is applied by Claude Code.

With the other levels: no merging. When two levels have a skill with the same name, one whole skill wins: organization > personal (user) > project. Note that your personal skill beats the project one, which is the opposite of settings. Skills with different names are all available.

If they contradict: only the winning skill loads, so there is nothing to reconcile.

The team commits this folder; you run it with /security-review <branch>, and disable-model-invocation: true means Claude never starts it on its own.

MARKDOWN
---
description: Reviews code changes for security vulnerabilities, authentication gaps, and injection risks
disable-model-invocation: true
argument-hint: <branch-or-path>
---

## Diff to review

!`git diff $ARGUMENTS`

Audit the changes above for:

1. Injection vulnerabilities (SQL, XSS, command)
2. Authentication and authorization gaps
3. Hardcoded secrets or credentials

Use checklist.md in this skill directory for the full review checklist.
📄 Project MCP servers · acme-app/.mcp.json · example

⚙️ Configuration: Claude Code reads it. The file decides which MCP servers Claude Code connects to. The model never sees it. The tool descriptions those servers send are read by the model, and they are guidance.

With the other levels: fields don't merge. When two scopes define the same server name, one whole entry wins: managed > local > project > user. Servers with different names all connect. Each person approves the servers in a project's .mcp.json once.

If they contradict: the winning entry is used as-is, and the others are ignored.

The team commits this at the project root; Claude Code asks each person to approve these servers once in an interactive session, and ${NOTION_TOKEN} comes from your shell, so the secret never lands in git.

JSON
{
  "mcpServers": {
    "notion": {
      "command": "npx",
      "args": ["-y", "@notionhq/notion-mcp-server"],
      "env": {
        "NOTION_TOKEN": "${NOTION_TOKEN}"
      }
    }
  }
}

Every other file in the map: full examples

These complete the map. Each one also opens from its ↗ link in the table.

📄 Organization level · /Library/Application Support/ClaudeCode/managed-settings.d/10-telemetry.json + 20-security.json · example

🔒 Enforced: Claude Code (the program) reads it. These keys are not advice to the model. Claude Code applies them in code: it checks permissions before every tool call and runs hooks at their events, whatever Claude decides.

With the other levels: first these files merge with managed-settings.json into one organization level. Inside that merge, a later file wins for single values, lists combine, and env merges per variable. Then that merged policy beats every other level.

If two levels contradict: code always gives one answer. A deny beats an allow at any level. For a single value, the higher level wins. See the trace table.

Two teams in IT each own one file in the drop-in folder. Claude Code reads managed-settings.json first, then these two in name order, and merges all three into one org policy.

JSON
// managed-settings.d/10-telemetry.json — owned by the platform team
{
  "cleanupPeriodDays": 30,
  "env": {
    "DISABLE_TELEMETRY": "1",
    "OTEL_SERVICE_NAME": "claude-code-acme"
  }
}
JSON
// managed-settings.d/20-security.json — owned by the security team
{
  "cleanupPeriodDays": 14,
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(~/.aws/**)"
    ]
  }
}

Merged result for these keys: cleanupPeriodDays = 14 (single value: 20-security.json comes later, so it wins). permissions.deny = the deny list from managed-settings.json plus these two entries, duplicates removed (Read(./.env) appears once). env = merged key by key, so both variables are set. JSON files can't hold comments; the // line above each block only labels the file here.

📄 Local level · acme-app/CLAUDE.local.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: nothing overrides anything. Claude Code loads all CLAUDE.md files together, in this order: organization → user → project → local, plus a subfolder's CLAUDE.md when Claude works in that folder. The more specific file is read last. This local file loads right after the project CLAUDE.md in the same folder.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

Only you have this file. Claude Code loads it after acme-app/CLAUDE.md, so it is read last. Add CLAUDE.local.md to .gitignore yourself so it never reaches the team.

MARKDOWN
# My notes for acme-app (not shared)

- My local database runs on port 5463 (docker compose), not 5432.
- Test account for manual checks: `dev-sam@acme.test`.
- I'm working on the refund flow this week; start there.
📄 User path rule · ~/.claude/rules/java-style.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: a rule file loads like a CLAUDE.md. With paths: it loads only when Claude touches a matching file. Your user rules load before the project rules, and neither overrides the other.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

You write this. It applies in every project on your machine, but because of paths: Claude Code loads it only when Claude reads or edits a matching file. User rules load before project rules.

MARKDOWN
---
paths:
  - "**/*.java"
---

# My Java style

- Use `record` for DTOs instead of a class with getters.
- Prefer `Optional` return values over returning `null`.
- Keep methods under ~30 lines; extract a private method otherwise.
📄 Organization subagent · /Library/Application Support/ClaudeCode/.claude/agents/security-auditor.md · example

🔀 Mixed. The body below the --- becomes the subagent's system prompt: guidance the model reads. The tools: line is enforced: Claude Code gives the subagent only those tools.

With the other levels: no merging. When two levels define the same name, one whole file wins: organization > --agents flag > project > user > plugin. Subagents with different names are all available.

If they contradict: the losing file is not read at all, so its lines can't conflict with the winner.

IT deploys this into the managed folder. Every developer gets the subagent, and a managed subagent beats a project or user subagent with the same name.

MARKDOWN
---
name: security-auditor
description: Audits changes for secrets, injection and auth problems. Use before any commit that touches auth or payment code.
tools: Read, Grep, Glob
---

You are ACME's security auditor. You only read code; you never edit it.

Check the changed files for:
1. Hardcoded secrets, tokens or passwords
2. SQL, command or template injection
3. Missing authorization checks on endpoints

Report each finding as: file:line, severity (high/medium/low), one-line fix.
📄 User subagent · ~/.claude/agents/explainer.md · example

🔀 Mixed. The body below the --- becomes the subagent's system prompt: guidance the model reads. The tools: line is enforced: Claude Code gives the subagent only those tools.

With the other levels: no merging. When two levels define the same name, one whole file wins: organization > --agents flag > project > user > plugin. Subagents with different names are all available.

If they contradict: the losing file is not read at all, so its lines can't conflict with the winner.

You write this. It is available in every project on your machine. A project subagent with the same name would beat it.

MARKDOWN
---
name: explainer
description: Explains unfamiliar code in plain, short English. Use when the user asks what some code does.
tools: Read, Grep, Glob
---

Explain the code the user points at.
- Start with one sentence: what it does and who calls it.
- Then walk through it step by step in short sentences.
- Name each technical term with its meaning the first time.
📄 Organization skill · /Library/Application Support/ClaudeCode/.claude/skills/release-checklist/SKILL.md · example

🔀 Mixed. The body is instructions Claude reads when the skill runs: guidance. The frontmatter (for example disable-model-invocation) is applied by Claude Code.

With the other levels: no merging. When two levels have a skill with the same name, one whole skill wins: organization > personal (user) > project. Note that your personal skill beats the project one, which is the opposite of settings. Skills with different names are all available.

If they contradict: only the winning skill loads, so there is nothing to reconcile.

IT deploys this folder. Everyone can run /release-checklist, and it beats a personal or project skill with the same name.

MARKDOWN
---
description: ACME's release checklist. Use when preparing or reviewing a production release.
---

Before a release, confirm each item and report ✅ or ❌:

1. All tests pass on the release branch (`mvn verify`).
2. The CHANGELOG has an entry for this version.
3. Database migrations are backward compatible.
4. No `TODO` or `FIXME` in the changed files.
📄 User skill · ~/.claude/skills/explain-diff/SKILL.md · example

🔀 Mixed. The body is instructions Claude reads when the skill runs: guidance. The frontmatter (for example disable-model-invocation) is applied by Claude Code.

With the other levels: no merging. When two levels have a skill with the same name, one whole skill wins: organization > personal (user) > project. Note that your personal skill beats the project one, which is the opposite of settings. Skills with different names are all available.

If they contradict: only the winning skill loads, so there is nothing to reconcile.

You write this folder. You run it with /explain-diff in any project, and Claude can also start it on its own when the description matches the task.

MARKDOWN
---
description: Explains the current git diff in plain English, file by file.
argument-hint: [base-branch]
---

## The diff

!`git diff ${ARGUMENTS:-main}`

Explain the diff above:
- one short paragraph per file: what changed and why it matters
- end with the risks a reviewer should check
📄 User command (old style) · ~/.claude/commands/standup.md · example

🔀 Mixed. The body is instructions Claude reads when the skill runs: guidance. The frontmatter (for example disable-model-invocation) is applied by Claude Code.

With the other levels: no merging. When two levels have a skill with the same name, one whole skill wins: organization > personal (user) > project. Note that your personal skill beats the project one, which is the opposite of settings. Skills with different names are all available.

If they contradict: only the winning skill loads, so there is nothing to reconcile.

The old way to make a slash command: one Markdown file, no folder. It still works and creates /standup. A skill folder does the same job and can also hold extra files, so new commands are usually skills.

MARKDOWN
---
description: Summarize what I did since yesterday for standup
---

Look at my git commits since yesterday (`git log --since=yesterday --author="$(git config user.name)"`)
and write three bullets: done, doing, blocked.
📄 Project command (old style) · acme-app/.claude/commands/fix-issue.md · example

🔀 Mixed. The body is instructions Claude reads when the skill runs: guidance. The frontmatter (for example disable-model-invocation) is applied by Claude Code.

With the other levels: no merging. When two levels have a skill with the same name, one whole skill wins: organization > personal (user) > project. Note that your personal skill beats the project one, which is the opposite of settings. Skills with different names are all available.

If they contradict: only the winning skill loads, so there is nothing to reconcile.

The team commits this. Anyone in the repo runs /fix-issue 123; $ARGUMENTS becomes 123.

MARKDOWN
---
description: Fix a GitHub issue by number
argument-hint: <issue-number>
---

Fix GitHub issue #$ARGUMENTS:
1. Read the issue with `gh issue view $ARGUMENTS`.
2. Find the code involved and fix it.
3. Add or update a test that fails without the fix.
4. Run `mvn test` and report the result.
📄 Organization MCP servers · /Library/Application Support/ClaudeCode/managed-mcp.json · example

🔒 Enforced: Claude Code reads it, and it is a lock. The model never sees this file. It decides which MCP servers exist at all.

With the other levels: nothing merges with it. When this file exists, Claude Code loads only its servers, plus any the org provides through managedMcpServers. Your ~/.claude.json and the project .mcp.json servers stop loading, and claude mcp add fails with an "exclusive control" error. Plugin servers and --mcp-config are blocked too.

If you want to add on top instead: IT uses the managedMcpServers key in managed-settings.json. Users keep their own servers, and a provided server wins over a user server with the same name.

IT installs this file with admin rights. It uses the same format as .mcp.json. It takes exclusive control: these are the only MCP servers anyone on the machine can use. An empty mcpServers map turns MCP off. IT can instead give everyone extra remote servers, without the lock, through the managedMcpServers key of managed-settings.json.

JSON
{
  "mcpServers": {
    "github": {
      "type": "http",
      "url": "https://api.githubcopilot.com/mcp/"
    }
  }
}
📄 User and local MCP scope · ~/.claude.json · example

⚙️ Configuration: Claude Code reads it. The file decides which MCP servers Claude Code connects to. The model never sees it. The tool descriptions those servers send are read by the model, and they are guidance.

With the other levels: fields don't merge. When two scopes define the same server name, one whole entry wins: managed > local > project > user. Servers with different names all connect. Each person approves the servers in a project's .mcp.json once.

If they contradict: the winning entry is used as-is, and the others are ignored.

Claude Code writes this file itself (sign-in, trust answers, MCP servers); you normally change it with claude mcp add, not by hand. Top-level mcpServers = user scope (every project). projects["<path>"].mcpServers = local scope (only that folder). It is not ~/.claude/settings.json.

JSON
{
  "mcpServers": {
    "hubspot": {
      "type": "http",
      "url": "https://mcp.hubspot.com/anthropic"
    }
  },
  "projects": {
    "/Users/sam/Projects/acme-app": {
      "mcpServers": {
        "stripe": {
          "type": "http",
          "url": "https://mcp.stripe.com"
        }
      }
    }
  }
}

claude mcp add --scope user hubspot … writes the top-level entry. claude mcp add --scope local stripe … (local is the default) writes the entry under the project path.

📄 Organization hook script · /usr/local/bin/acme-audit-bash.sh · example (path is IT's choice)

🔒 Enforced: this is code. Claude Code runs the script at its event. Exit code 2 blocks the action, whatever the model wanted.

With the other levels: hooks from every level run. None replaces another.

If they disagree: if any one hook blocks, the action does not run.

IT installs this script and registers it in managed-settings.json under hooks.PreToolUse. Claude Code runs it before every Bash command and sends the pending tool call as JSON on stdin. Exit 0 = let it run; exit 2 = block it, and Claude Code shows the stderr text to Claude.

BASH
#!/usr/bin/env bash
# PreToolUse hook (matcher: Bash). Logs every command; blocks pushes to main.
input=$(cat)                                        # JSON from Claude Code on stdin
cmd=$(jq -r '.tool_input.command' <<<"$input")
echo "$(date -u +%FT%TZ) $USER $cmd" >> /var/log/acme/claude-bash.log
if [[ "$cmd" =~ git\ push.*(main|master) ]]; then
  echo "Blocked by ACME policy: push to a branch and open a PR instead." >&2
  exit 2
fi
exit 0
📄 User hook script · ~/.claude/hooks/check-bash.sh · example

🔒 Enforced: this is code. Claude Code runs the script at its event. Exit code 2 blocks the action, whatever the model wanted.

With the other levels: hooks from every level run. None replaces another.

If they disagree: if any one hook blocks, the action does not run.

You write this and register it in ~/.claude/settings.json under hooks.PreToolUse (matcher Bash). It runs in every project, together with the org hook: hooks from all levels run, none replaces another.

BASH
#!/usr/bin/env bash
# PreToolUse hook (matcher: Bash). Stops me from deleting things by accident.
cmd=$(jq -r '.tool_input.command')                  # stdin = the pending tool call
if [[ "$cmd" =~ (^|[[:space:]])rm[[:space:]]+-rf ]]; then
  echo "rm -rf is blocked by my user hook. Delete the specific file instead." >&2
  exit 2                                            # 2 = block; stderr goes to Claude
fi
exit 0
📄 Project hook script · acme-app/.claude/hooks/format.sh · example

🔒 Enforced: this is code. Claude Code runs the script at its event. Exit code 2 blocks the action, whatever the model wanted.

With the other levels: hooks from every level run. None replaces another.

If they disagree: if any one hook blocks, the action does not run.

The team commits this and registers it in .claude/settings.json under hooks.PostToolUse (matcher Edit|Write) as ${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh. ${CLAUDE_PROJECT_DIR} makes the path work wherever each person cloned the repo.

BASH
#!/usr/bin/env bash
# PostToolUse hook (matcher: Edit|Write). Formats every Java file right after Claude edits it.
file=$(jq -r '.tool_input.file_path')               # stdin = the tool call that just ran
if [[ "$file" == *.java ]]; then
  google-java-format --replace "$file"
fi
exit 0

Straight answers

Is the org-level file settings.json or CLAUDE.md? Both can exist, side by side in the same system folder. managed-settings.json holds enforced settings. CLAUDE.md holds instructions. Note the name: at org level it is managed-settings.json, not settings.json. IT can also skip the separate CLAUDE.md and put the text in the claudeMd key inside managed-settings.json.

Do settings only configure hooks? No. Settings files hold permissions (allow / ask / deny), hooks, env, model, statusLine, outputStyle and many more keys. Things that are not in settings: instructions (CLAUDE.md, .claude/rules/), subagents (agents/), skills (skills/), and project MCP servers (.mcp.json). Your personal MCP servers live in ~/.claude.json.

Where are agents defined? As markdown files: .claude/agents/<name>.md for the project, ~/.claude/agents/<name>.md for you. You can also pass them as JSON with claude --agents '…' for one session, or get them from a plugin's agents/ folder.

Are there org-level agents and skills? Yes. The admin puts them inside the managed settings folder: .claude/agents/ and .claude/skills/<name>/SKILL.md there (macOS: /Library/Application Support/ClaudeCode/.claude/agents/). A managed agent beats a project or user agent with the same name. An enterprise skill beats personal and project skills with the same name. The other way to share across a team is a plugin. An org-level rules/ folder is not documented.

Where do MCP servers go? Team: .mcp.json at the project root, committed. Just you: ~/.claude.json (user scope = all projects, local scope = this project only), written by claude mcp add --scope user|local. Org, two different tools: managed-mcp.json in the managed folder is a lock (only its servers load; nobody can add others), while the managedMcpServers key in managed settings adds remote servers and users keep their own.

Does CLAUDE.md enforce anything? No. The docs call it "context, not enforced configuration". Claude usually follows it, but nothing forces it. If something must never happen, use a permissions.deny rule or a PreToolUse hook. Claude Code applies those whatever the model decides.

Rule or guidance? What is enforced, and what Claude only reads

Short answer: settings are enforced; CLAUDE.md is guidance. Claude tries to follow CLAUDE.md, and usually does. But nothing stops it when it doesn't. A settings rule does not depend on Claude at all: Claude Code (the program) checks it in code, every time.

The official docs say it word for word:

"Settings rules are enforced by the client regardless of what Claude decides to do. CLAUDE.md instructions shape Claude's behavior but are not a hard enforcement layer." — Memory docs↗

"CLAUDE.md content is delivered as a user message after the system prompt, not as part of the system prompt itself. Claude reads it and tries to follow it, but there's no guarantee of strict compliance, especially for vague or conflicting instructions." — Memory docs, troubleshooting↗

Who reads what: the model or the program?

Two different readers. This is the whole trick.

CLAUDE.md, .claude/rules/*.md settings.json → permissions, hooks, sandbox
Who reads it The model. Claude Code pastes the text into the conversation, and Claude reads it like a message from you. Claude Code, the program. The model never gets a vote.
What it is Advice: conventions, commands, "please don't…" A check that runs in code: allow, ask, deny, run a script
When it acts When Claude decides what to do After Claude decides, before the action runs
Can Claude ignore it? Yes. Rarely with clear, short instructions, more often with vague, long or conflicting ones. No. A denied tool call is refused, whatever Claude wanted.
What breaks it Vague wording, a 600-line file, two instructions that contradict each other, a long session Nothing the model says. Only someone editing the settings file.
Good for "We use mvn verify", "DTOs are records", "explain in plain English" "Never read .env", "never push to main", "always format after edit"

Think of a new coworker. CLAUDE.md is the team wiki they read on day one. They follow it almost always. Settings are the badge reader on the server-room door: it doesn't matter what they decided, the door doesn't open.

Same rule, written both ways

The rule: "never push to main".

📄 As guidance · acme-app/CLAUDE.md · Claude reads it, usually follows it

📖 Guidance: the model reads it. This is the same line as any other CLAUDE.md line. Claude usually won't push to main, but nothing stops it.

With the other levels: it loads together with every other CLAUDE.md. Nothing overrides it, and it overrides nothing.

If another line contradicts it (or you type "push it to main, it's urgent"): Claude may follow either one.

The team writes this line. Claude Code loads it as text into the conversation. Claude reads it and, almost always, doesn't push. But if you later type "just push it to main, it's urgent", or the session runs long, or another file says something different, Claude may push anyway. Nothing in code stops it.

MARKDOWN
# acme-app

- Never push to `main`. Push to a feature branch and open a PR.
📄 As enforcement · acme-app/.claude/settings.json · Claude Code blocks it, every time

🔒 Enforced: Claude Code reads it. Claude Code checks the deny rule before the Bash call runs. The model gets no vote.

With the other levels: deny lists from every level are combined. An allow at any level cannot reopen it.

If something contradicts it (an allow rule, a CLAUDE.md line, or you asking in chat): the deny still wins. Only someone editing a settings file can remove it.

The team commits this. When Claude tries git push origin main, Claude Code matches the deny rule and refuses the tool call before it runs. Claude only sees "permission denied" and has to choose something else. What Claude "wanted" doesn't matter.

JSON
{
  "permissions": {
    "deny": [
      "Bash(git push origin main*)",
      "Bash(git push --force*)"
    ]
  }
}

Best practice is both: the CLAUDE.md line tells Claude why and what to do instead (so it doesn't waste a turn hitting the wall). The deny rule makes sure it can't happen. For checks that need logic (look inside the command, check a file path, run a formatter), use a hook script instead of a deny rule. Example hook scripts.

It's the same split outside Claude Code

The exam asks this in every runtime. Only the names change.

Runtime Guidance (the model reads it; it usually follows) Enforcement (code runs; the model can't skip it)
Claude Code CLAUDE.md, .claude/rules/, skill text, a subagent's body permissions allow/ask/deny, hooks, sandbox, managed settings, --allowedTools / --disallowedTools
Messages API (your own app) system prompt, user messages, tool descriptions, few-shot examples Your harness code: which tools you put in tools[], checking tool inputs before you run them, an approval gate before risky actions, output_config.format / strict: true (the reply is guaranteed to match the schema)
Agent SDK system_prompt, CLAUDE.md files loaded via setting_sources allowed_tools / disallowed_tools, permission_mode, the can_use_tool callback, hooks
MCP server tool and prompt descriptions the server's own code: input validation, auth, what the tool actually does

So in the API world, the system prompt is the CLAUDE.md: guidance. And your code is the settings.json: enforcement. Writing "IMPORTANT: NEVER refund more than $500" in capital letters in the system prompt is still guidance. A code check if amount > 500: require_approval() is enforcement.

One trap: "Respond only in JSON" in the system prompt is guidance, so the model may add text around the JSON. output_config.format with a JSON schema is enforcement. The API constrains the output so it matches the schema (the documented exceptions: a refusal, or a reply cut off by max_tokens). Structured output, in Domain 3.

How to pick the answer on the exam

Read the stem for these words:

The stem says… It wants… Right answer looks like
"must never", "always", "guarantee", "every time", "regardless", "compliance", "audit", "block", "prevent", "deterministic" Enforcement Claude Code: permissions.deny, a PreToolUse hook, managed settings. API: a code check, a tool removed from tools[], an approval gate, output_config.format
"conventions", "style", "the team's commands", "know about", "context", "prefer", "explain", "consistent across the team" Guidance Claude Code: project CLAUDE.md (shared in git), .claude/rules/ for some paths. API: the system prompt
"Claude sometimes ignores our CLAUDE.md" Better guidance Make it specific and short, remove conflicting lines, move path-only rules to .claude/rules/. If it truly must happen, switch to a hook

Two traps to spot. A distractor is a wrong answer choice that is written to look right.

Check yourself: guidance or enforcement?
  1. "The team wants Claude to use pnpm, not npm, in this repo." → Guidance: one line in the project CLAUDE.md.
  2. "Compliance requires that Claude can never read files under secrets/." → Enforcement: permissions.deny: ["Read(./secrets/**)"], ideally in managed settings.
  3. "Your API support agent must never issue a refund over $500 without a human." → Enforcement in your code: the refund tool checks the amount and pauses for approval. A system-prompt line alone is not enough.
  4. "Your API extraction job must always return valid JSON for the parser." → Enforcement: output_config.format with a schema, not "reply in JSON" in the prompt.
  5. "Every Java file must be formatted after Claude edits it." → Enforcement: a PostToolUse hook that runs the formatter. "Always format your code" in CLAUDE.md would only be guidance.

CLAUDE.md, settings.json, and .claude.json: which file does what?

These are separate kinds of files. They are not three competing versions of one configuration. The names say CLAUDE, not “cloud.” .claude is a directory; .claude.json is a file. Open the file guide glossary.

Instruction files: guidance loaded into context

Scope Location Example purpose
User ~/.claude/CLAUDE.md Your general ways of working.
Project CLAUDE.md or .claude/CLAUDE.md Conventions shared in the repository.
Personal project CLAUDE.local.md Your notes for this project; keep it out of git.
Subdirectory services/billing/CLAUDE.md Guidance for that part of the code.
Organization Managed CLAUDE.md at the location for each platform Guidance for the model across the organization.
📄 Nested (subdirectory) level · acme-app/services/billing/CLAUDE.md · example

📖 Guidance: the model reads it. Claude Code does not check this file. It pastes the text into the conversation, and Claude reads it like a message from you. Claude usually follows it, but nothing guarantees it.

With the other levels: nothing overrides anything. Claude Code loads all CLAUDE.md files together, in this order: organization → user → project → local, plus a subfolder's CLAUDE.md when Claude works in that folder. The more specific file is read last. A subfolder file loads only when Claude reads or edits files in that folder, after the project root file.

If two lines contradict (say the org file says "Java 21" and the project file says "Java 17"): there is no winner. Claude may follow either one. Fix it by removing the conflict. If one rule must win every time, it is not a CLAUDE.md job: use a settings rule or a hook. (Rule or guidance?)

Claude Code does not load this file at session start; it loads it when Claude reads files under services/billing/.

MARKDOWN
# services/billing

- Money amounts are integer cents. Never use floats.
- Ledger writes go through `postEntry()`; never write SQL INSERTs directly.
- Run `mvn -pl services/billing test` after any change in this folder.

Claude Code adds every matching Markdown file to the context; the files add up. Files in parent folders load when the session starts. Files in nested folders can load later, when Claude reads files in that folder. This is not like JSON, where one key replaces another. A more specific instruction can guide the model, but Markdown is guidance only. It is not a hard permission limit. Official memory documentation↗.

Settings files: runtime configuration and precedence

Scope Location
User ~/.claude/settings.json
Shared project .claude/settings.json
Personal project .claude/settings.local.json
Organization Managed settings sources, including managed-settings.json

Settings files configure permissions, hooks, the preferred model, and other supported behavior. Claude Code enforces them. The order, strongest first, is: managed → command line → project local → shared project → user. For a single-value key, the stronger source wins. Lists, such as permission rules, are combined. After combining, an allow rule still does not cancel a matching deny. Some keys have their own exceptions; check them in the reference.

~/.claude.json is a different file. Claude Code itself writes it, to store state such as folder-trust decisions, MCP configurations, and global preferences. It is not another level of settings.json. A project file named claude.json is also not the project version of it. Use Claude Code’s own commands to change it, instead of editing it by hand. Official settings documentation↗.

MCP configuration: scope is not the same as file location

MCP scope Where Claude Code stores it Who uses it?
User User-level entries in ~/.claude.json You, in all projects.
Local Entries stored under the project path in ~/.claude.json You, in one project.
Project .mcp.json at the project root Teammates who use the shared configuration.

The surprising part: Local-scope MCP configuration for a project lives in a file in your home folder. But local runtime settings for a project live in .claude/settings.local.json. So “local” says who the entry applies to (you, in this project). It is not one fixed filename. If two scopes define an MCP server with the same name, local wins over project, and project wins over user. Managed restrictions still apply on top. To view or manage connections, use claude mcp list, claude mcp get <name>, and the --scope option of the MCP commands. Official MCP scope documentation↗.

One project, three different jobs · concrete example

Imagine a billing repository with three needs: Java naming conventions, a check before each edit, and access to an internal document server.

Requirement Put it here Why
“Use PascalCase for Java class names.” Project CLAUDE.md Explains the convention to the model (guidance).
Register a naming-check hook Project .claude/settings.json Tells Claude Code when to run the checking code.
Implement the naming check A script that the hook names The script does the real checking (enforcement).
Share the document-server connection Project .mcp.json Describes how to connect to the MCP server.
Set approval or denial for its tools Settings permissions rules Connecting and permitting are different jobs.
Keep a personal test MCP connection MCP local scope Claude Code stores it under this project in ~/.claude.json.

If you write a permissions JSON block inside CLAUDE.md, it is still only text for the model; it does not become a setting. In the same way, a sentence about naming inside some random settings key does not create a naming checker.

Three hierarchies to keep separate

Question Inspect
What instructions did the model receive? Which Markdown files loaded, and the current context.
Which Claude Code setting or permission applies? Settings sources, their order, and how rules are checked.
Which server definition provides these MCP tools? MCP scope, and which server wins for a name.

Inside Claude Code, use /memory to find instruction files and /context to see what is loaded in the context. Use /config and /status to see settings and where they came from, and /mcp to see server status. A plain Messages API call from your own app does not load any of these Claude Code files. In an Agent SDK application, your code sets options that control what it loads.

Continue with tool availability and permission examples and whether an agent loads CLAUDE.md.

Recall: Markdown guides the model. Settings configure the runtime. Hooks run checks. MCP configuration connects servers. .claude.json is state that Claude Code stores for itself; it is not “project settings with a different name.” Documentation checked September 22, 2026; the examples use the standard locations.

CLAUDE.md — the memory hierarchy

🧭 Your role here: Claude Code comes with the harness (the software that runs the agent) already built. So your job as architect moves up one level: you write the standing setup that every session starts from. With CLAUDE.md, you set context and rules once, and every future session gets them (guidance). With skills, you package expertise. With hooks, you make rules run the same way every time (enforcement). With settings, you draw the permission boundary (enforcement). In Domain 2, "configuring a tool" and "designing a system" turn out to be the same work.

flowchart TD
  U[User CLAUDE.md] --> C[Combined model instructions]
  P[Project CLAUDE.md, checked in] --> C
  N[Nested CLAUDE.md, loaded when relevant] --> C
  E[Managed settings] --> R[Runtime configuration and enforcement]
  C --> M[Model proposes action]
  M --> R

Managed settings are runtime configuration. They are not part of the Markdown memory ladder. An organization's instruction files still only guide the model. Managed configuration can enforce the controls that Claude Code supports. Do not put these two mechanisms into one precedence diagram.

🎯 Scenario 2 (team code generation — one of the six scenarios) often asks about precedence. For permissions (enforced): enterprise policy > project settings > user settings. For memory (guidance): the project CLAUDE.md covers the repo, while ~/.claude/CLAUDE.md goes with the person. Shared standards → project file in git. Personal preferences → user file.

🔍 See it run — four memory layers firing in one session at a fintech monorepo

Start: an engineer opens Claude Code in payments-monorepo/ and asks it to add a retry to the charge client.

Here is what really loaded when the session started, from the top of the diagram down:

  1. [Organization instructions plus enforced settings] — IT provides a reminder about data handling as context (guidance). Separately, IT configures permission and network controls (enforcement). The reminder teaches the rule; the enforced controls limit actions.
  2. [~/.claude/CLAUDE.md — user] — the engineer's personal file: "Answer concisely. I prefer vitest over jest." It goes with the engineer to every repo.
  3. [repo/CLAUDE.md — project] — the team checked it into git: "pnpm, never npm. All money amounts are integer cents. Run pnpm test:unit before declaring done." It guides everyone in this repo.
  4. [subdir CLAUDE.md — nested] — nothing yet. When Claude reads a file under packages/charge-client/, that package's CLAUDE.md ("this package must stay dependency-free") loads only when needed. It used no context until the work reached that folder.

Now separate instructions from settings. The user file says "concise". The project file says "run tests before done". These do not conflict, so both apply. But when the model proposes npm install retry, the project instruction asks for pnpm. If instructions conflict, remove the conflict; do not assume a guaranteed order among written instructions. And when the model tries to curl an npm advisory page, an enforced managed permission setting can deny it. Lower settings and prompt text cannot change that. It is the one layer that no local file can beat.

For the exam: shared standards → project file (in git, for the whole team). Personal taste → user file. Organization policy → enterprise layer. Nested files save context: knowledge about a deep folder loads only when work enters that folder.

🧭 Each layer here is a real file you can click, below — the layers as real files.

Team instructions versus enforced naming rules

Why the checked-in rule did not guarantee compliance

A team naming rule in the repository feels like it should beat a developer's personal preference. That is a fair expectation, because the team owns the repo. But sharing a rule and enforcing a rule are different things. Git shares the file. Claude Code loads the matching instructions. The model must still follow them when it writes code. When a personal instruction conflicts with the team's, the generated code can follow the wrong one.

The filename is CLAUDE.md, not cloud.md. Once the model has already produced a wrong name, the question changes. It is no longer "which Markdown file shares the convention best?" It is "how do we make sure a wrong name cannot be accepted, even when the model follows the wrong preference?" Keep the convention in the team's CLAUDE.md (guidance). Add enforcement that runs code. Only moving the text to another file is not enough.

Three mechanisms, three different jobs

flowchart TD
    U[Personal CLAUDE.md instructions] --> C[Model context contains instructions]
    P[Team CLAUDE.md instructions] --> C
    C --> G[Model proposes Java changes]
    S[Effective settings configure hooks and permissions] --> H[Runtime checks the operation]
    G --> H
    H --> V[Run naming validator on relevant code]
    V --> PASS[Compliant change can proceed]
    V --> FAIL[Violation: reject or request repair]
    PASS --> CI[Required CI check before merge]
Mechanism Plain meaning What it cannot promise
Instruction files (guidance) Tell the model the team's conventions. That conflicting text will always be sorted out correctly.
Settings (Claude Code config) Configure supported Claude Code behavior, including which hooks run. That any sentence about Java naming becomes a checker.
Validator and acceptance gate (enforcement) Check the real code and reject what breaks the implemented checks. That requirements the validator does not check are met.

Worked example: refactoring, tests and test files

One team has five rules. Each rule goes to the mechanism whose promise matches how strict the rule is. All of these are Claude Code files in the repo.

The team's rule Where it goes Why there
"Refactor in small steps, keep public API names, propose a plan before touching more than 10 files" CLAUDE.md Judgment and preference (guidance). The model should consider it; an exception now and then is acceptable.
"Run the tests with npm test; tests live in tests/ and use vitest" CLAUDE.md and settings.json allow The how-to is context (CLAUDE.md). The allow rule only removes the approval prompt for that exact command.
"Never push to main" settings.json deny A hard limit on which command may run at all (enforcement). A permission rule blocks it, whatever the model decides.
"Never edit an existing test to make it pass; adding new tests is fine" A PreToolUse hook It needs logic (does this test file already exist?). A deny pattern cannot say "new is fine, existing is not".
"Format every file Claude writes" A PostToolUse hook It must happen after every edit, every time, not only when the model remembers.

1 · CLAUDE.md: context the model reads and weighs

MARKDOWN
# Checkout service

## How we work
- Refactor in small, reviewable steps. Keep public API names stable.
- Propose a plan and wait for approval before changing more than 10 files.
- Run the test suite with `npm test` (vitest; tests live in tests/).
- Fix the code, not the test: existing tests are the specification.
  (A hook enforces this, so an edit to an existing test is rejected.)

2 · .claude/settings.json: what may run at all, plus hook registration

JSON
{
  "permissions": {
    "allow": ["Bash(npm test)", "Bash(npm run lint)"],
    "deny": ["Bash(git push origin main*)", "Bash(git push --force*)"]
  },
  "hooks": {
    "PreToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/protect_tests.py\"" }] }
    ],
    "PostToolUse": [
      { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write" }] }
    ]
  }
}

3 · .claude/hooks/protect_tests.py: the rule that needs logic

PYTHON
import json
import os
import sys

call = json.load(sys.stdin)                      # the pending tool call, sent by Claude Code
path = call.get("tool_input", {}).get("file_path", "")
is_test = "/tests/" in "/" + path.lstrip("./")
if is_test and os.path.exists(path):
    print("Existing tests are the specification: fix the code, not the test. "
          "New test files are allowed.", file=sys.stderr)
    sys.exit(2)                                  # exit code 2 = block; stderr goes back to Claude
sys.exit(0)                                      # anything else proceeds

What happens when Claude tries each action

Claude tries to… What decides Result
Run npm test settings allow Runs with no approval prompt
Rename a function across 40 files CLAUDE.md guidance Usually proposes a plan first. This is not guaranteed, which is fine for a preference
Change an assertion in tests/cart.test.ts PreToolUse hook Blocked (exit 2). Claude reads the message and fixes the code instead
Create tests/discount.test.ts PreToolUse hook Allowed, because the file is new
Write src/cart.ts PostToolUse hook The write runs; then Prettier formats that file every time (the hook reads the edited path from the tool call on stdin)
git push origin main settings deny Refused before it runs

Look for the strength word in the exam question. "Prefer", "convention", "explain how", "our commands are" → CLAUDE.md (guidance). "Must never", "guarantee", "compliance", "every time" → settings (which tool or command may run) or a hook (a condition that needs logic, or an action after every edit) (enforcement). If a rule is only in CLAUDE.md and "it happened anyway", move the rule into enforcement and keep the explanation in CLAUDE.md.

Read the filenames before applying a hierarchy

File Intended ownership Is it the team's shared rulebook?
~/.claude/CLAUDE.md Your personal instructions for all projects No. It goes with that developer.
repo/CLAUDE.md Instructions for the repository Yes. Commit shared conventions here.
repo/CLAUDE.local.md Your personal instructions for one project No. Keep private preferences out of git.
repo/.claude/settings.json Shared project configuration Commit supported settings and hook registration here.
repo/.claude/settings.local.json Your personal configuration for one project Local overrides. It is not where the team's policy lives.

CLAUDE.local.md and settings.local.json are different files. If you move Markdown into CLAUDE.local.md, it does not gain the fixed, enforced behavior of settings.local.json. Loaded instruction files only add context. There is no guaranteed “last file wins” rule that safely settles conflicting instructions. Official memory documentation↗

Overlay versus union: which operation is happening?

For normal settings conflicts, the general order is managed → CLI → project-local → shared project → user. “Project-local” means .claude/settings.local.json. It does not mean the user file ~/.claude/settings.json, which applies to all projects. Some keys have exceptions; check each key.

Overlay (one value replaces another): When two scopes set the same single-value key, the value from the stronger scope applies. Other keys from the weaker scope stay. For example, a project spinnerTipsEnabled: false beats a user true, unless a stronger scope also sets it.

Union (lists are added together): Normal permission lists collect entries from every scope; one list does not replace another. Collecting the rules is one step. Deciding whether one action is allowed is a second step. An allow entry never gets around a matching deny.

Instructions add up: Claude Code combines the loaded Markdown text into the model's context. This is not value replacement, and it is not a permission check that code runs. Do not call all three behaviors just “the hierarchy.” Settings precedence and list merging↗

Simulate the Java naming example

Suppose the team requires Java class names like InvoiceService, but a personal instruction prefers invoice_service. These names are illustrative.

  1. The model sees the team convention, but it still writes class invoice_service.
  2. A configured Java linter checks the new code against the team's class-name pattern and reports a violation.
  3. Someone must fix the change before a required acceptance check can pass. Claude can write the fix, or a person can.
  4. The same check runs in CI, so edits made outside Claude Code are also checked before merge.

The design works because bad output is rejected. It does not work by making the model unable to produce bad output. For generated code, “guaranteed on every run” should mean this: the violations that the check covers cannot pass the enforced gate. It does not mean every draft along the way is correct.

Where the hook is configured and what the checker actually does

Complete illustrative repo/.claude/settings.json:

JSON
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "bash \"$CLAUDE_PROJECT_DIR/scripts/check-java-names.sh\""
          }
        ]
      }
    ]
  }
}
🏭 Production shape: the checker script the hook runs

The JSON above only registers the hook. This is the script it points to. Claude Code sends the event JSON on standard input; the script checks only Java files with Checkstyle and the team's naming rules. PostToolUse runs after the edit, so it cannot undo it. Exit code 2 sends the violations (stderr) back to Claude, which then fixes the names.

BASH
#!/usr/bin/env bash
# scripts/check-java-names.sh — PostToolUse hook; event JSON arrives on stdin
set -euo pipefail

file=$(jq -r '.tool_input.file_path // empty')
[[ "$file" == *.java ]] || exit 0                  # not a Java file: nothing to check

cd "$CLAUDE_PROJECT_DIR"
# Checkstyle exits non-zero when it finds violations.
if ! report=$(java -jar tools/checkstyle.jar -c config/checkstyle/naming.xml "$file" 2>&1); then
  {
    echo "Java naming rules failed for $file. Fix these names:"
    echo "$report" | grep -E '\[(ERROR|WARN)\]' | head -20
  } >&2
  exit 2                                            # exit 2: stderr is shown to Claude
fi
exit 0                                              # clean: Claude continues normally

Where it lives: scripts/check-java-names.sh next to config/checkstyle/naming.xml; the jar path is your choice. The machine needs jq and Java.

The script named here must exist. It should run the project's Java checker (for example, Checkstyle with the team's real naming rules), read the result, and report violations in the output format that hooks support. The JSON only tells Claude Code when to run the script. It contains no Java naming logic. This configuration alone does not fully enforce anything.

This example uses PostToolUse to check code after an edit. It does not undo the write. Also, matching Write/Edit does not cover a Bash command, or an outside editor that changes files. A PreToolUse validator can reject a proposed operation before it runs. But it must understand the proposed change, cover the right tools, and check the proposed result correctly. Registering the event does none of this by itself. For acceptance across the team, run the checker as a required CI check and set up merge protection. Local hooks give feedback earlier. They do not replace that final acceptance gate. Hook events and control behavior↗

PreToolUse or PostToolUse? May the bad change happen at all?

The example above uses PostToolUse. It lets Claude write the file, then finds bad names and asks Claude to fix them. That fits when a bad change may exist for a moment and get fixed. When the rule is "this must never be created", you need the hook that runs before the write.

PreToolUse PostToolUse
When Claude Code runs it After Claude asks for the tool call, before Claude Code runs it After the tool already ran
What the script sees The pending call: tool_input.file_path, the new content, the Bash command The same call, plus the tool's result
Exit code 2 means The call is blocked. The file is never written. The reason (stderr) goes to Claude, which can retry with a correct name. The file is already written. The reason goes to Claude, which can fix it in a next step. Nothing is undone.
The stem says "never land", "must not be created", "block", "prevent", "guarantee … never" "after every edit", "format", "lint and report", "normalize the result"
Naming example Check file_path against ^V[0-9]+__[a-z0-9_]+\.sql$ and block a bad migration name Check the class names inside a Java file after an edit, and have Claude fix them

Memory hook: Pre = the gate (stops it). Post = the cleanup crew (reacts to it). Both are enforced code. They differ only in when they run, and only Pre can stop the action.

📄 Project hook script · acme-app/.claude/hooks/check-migration-name.sh · example (PreToolUse, blocks bad names)

🔒 Enforced: this is code. Claude Code runs it before every Write. Exit code 2 blocks the write, whatever the model wanted.

With the other levels: hooks from every level run. None replaces another.

If they disagree: if any one hook blocks, the write does not happen.

The team commits this script and registers it in .claude/settings.json under hooks.PreToolUse with "matcher": "Write". Claude Code sends the pending call as JSON on stdin.

BASH
#!/usr/bin/env bash
# PreToolUse hook (matcher: Write). Blocks migration files with a bad name BEFORE they are written.
file=$(jq -r '.tool_input.file_path // empty')
[[ "$file" == */db/migration/* ]] || exit 0                  # not a migration: allow
name=$(basename "$file")
if [[ ! "$name" =~ ^V[0-9]+__[a-z0-9_]+\.sql$ ]]; then
  echo "Blocked: '$name' does not match V<number>__<lower_snake_case>.sql (e.g. V3__add_table.sql). Use that format." >&2
  exit 2                                                      # 2 = block; stderr goes to Claude
fi
exit 0
JSON
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write",
        "hooks": [
          { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-migration-name.sh" }
        ]
      }
    ]
  }
}

Claude tries V3__AddTable.sql → the hook blocks it → Claude reads the reason and writes V3__add_table.sql instead. The bad name never lands.

Decision rule — where a rule must live to be enforced:

Mental model: Git makes a file available to teammates. Loading makes the file visible to the model. A validator decides whether the real code follows a rule that someone implemented. A required gate decides whether that result can be accepted.

Continue with hook registration and script location, settings precedence, and hooks.

Does this agent load CLAUDE.md?

“Agent” describes behavior. The runtime decides how instructions are loaded. A coordinator is a role an agent plays; it is not the name of an API. A research coordinator can run inside Claude Code, use the Claude Agent SDK, or be your own Python loop around the Messages API. All of them call the model in the end, but the software around the model is different.

The filename is CLAUDE.md, not cloud.md. It can hold research steps, client preferences, or instructions for one project. It is not only for general coding rules.

Implementation Who reads the file? What should you assume?
Custom application using anthropic.Anthropic().messages.create(...) Your application code. If you want the file, your code must read it and send its text. The API never finds local Markdown files by itself.
Claude Code Claude Code's own instruction-loading system. Matching project and folder instructions can enter the context.
Claude Agent SDK The SDK runtime. Its settings sources and working folder decide what loads. Check the configuration and the installed version. Do not confuse this SDK with the basic Anthropic API client.
flowchart TD
    R["Coordinator: a role"] --> A["Custom Messages API application"]
    R --> B["Claude Code or Agent SDK runtime"]
    A --> C["Application assembles system and messages explicitly"]
    B --> D["Runtime loads applicable instructions and assembles context"]
    C --> M["Model receives request content"]
    D --> M

The exact clue in the coordinator question

The clue is a given fact in the question, such as “the research coordinator's CLAUDE.md defines the default research methodology.” A sentence like that tells you the system already loads that file. Guidance from files is already working. So the runtime acts like Claude Code's hierarchy of instruction files, not like a plain API call that ignores files. Read the sentence as a fact about the loader, even when the question never names the runtime.

Do not use the shortcut “CLAUDE.md is mentioned, so it must apply.” Another scenario can describe a custom service that never loads the file; there, the honest answer is no. Find the loader the question states, or the behavior that already exists. When the question establishes guidance from files, nested CLAUDE.md files scoped to folders are the matching mechanism. When it does not, first ask which runtime the system uses and how it loads instructions. Only then choose a file.

The client-directory example, with the loader made explicit

Imagine this workspace is used by Claude Code:

TEXT
research/
  CLAUDE.md
  clients/
    acme/
      CLAUDE.md

The root file says: “Default citation style is Chicago. For client work, use the client-specific citation and source rules where provided.” The Acme file says: “For Acme reports, use APA 7 and peer-reviewed sources only.” Start a new client session from research/clients/acme/. Under the folder-loading rules, both the parent guidance and the client guidance are then available. For normal research, start in the normal workspace. This makes the scope clear. You do not rely on conflicting text to sort itself out.

A nested client folder organizes reusable guidance around one client's workspace. It is not a security wall between clients. Changing folders is not guaranteed to remove context that the conversation already loaded. Use sessions with the right scope, and access controls in your application. Nested instructions are guidance. They do not reliably enforce which sources are allowed. When those rules are required, check source details and citations in your application code. Claude Code memory behavior↗.

Agent SDK route: make loading explicit in configuration

The Agent SDK is a full agent runtime; it is not just messages.create. The current documentation says the default query() options turn on the project and user settings sources. If you set setting_sources yourself, that list decides what loads; an empty list turns these sources off. CLAUDE.md content goes into the conversation context. It does not replace the system prompt. Loading the file is a separate choice from choosing the claude_code system-prompt preset. Pin your SDK version and check it; do not assume old defaults. SDK instruction-loading documentation↗.

PYTHON
import asyncio
from claude_agent_sdk import query, ClaudeAgentOptions

async def main():
    async for message in query(
        prompt="Research the commissioned Acme report using this workspace's guidance.",
        options=ClaudeAgentOptions(
            cwd="/absolute/path/research/clients/acme",
            setting_sources=["project"],
            system_prompt="You coordinate evidence-based research. Identify unsupported claims.",
            max_turns=5,
        ),
    ):
        print(message)

# Illustrative SDK call; requires an installed/configured SDK and real workspace.
# asyncio.run(main())

cwd chooses the workspace folder. setting_sources explicitly includes the project guidance. These are Agent SDK options. Do not copy them into a Messages API request. The example gives a research role, not a coding role. A real application must also configure permissions and the research tools it offers. Loading files alone does not give access and does not build a research pipeline.

Custom API route: your code chooses what to send

For a custom service, your plain-API instinct is right. Your application can get the client rules from a database, a configuration file, or a trusted instruction document, and then build the request itself. Writing a local path in a message does not upload that file's content. This offline Python example prints a complete request. It does not call the model:

PYTHON
import json

# In a service, choose these from trusted configuration using authorized client identity.
default_rules = "Coordinate evidence-based research. Cite the sources used."
client_rules = "For this Acme report, use APA 7 and peer-reviewed sources only."
request = {
    "model": "YOUR_SUPPORTED_MODEL_ID",
    "max_tokens": 1500,
    "system": default_rules + "\n" + client_rules,
    "messages": [{
        "role": "user",
        "content": [{"type": "text", "text": "Prepare an outline for the commissioned remote-work report."}]
    }]
}
print(json.dumps(request, indent=2))
🏭 Production shape: the real call with trusted rules

In production the service sends the request instead of printing it. The client reads ANTHROPIC_API_KEY from the environment, the model id comes from configuration, and the rules come from your trusted store, chosen by the client id that your auth layer verified. Your code also checks stop_reason before it uses the text.

PYTHON
import os

import anthropic

from app.config import rules_for_client  # your trusted config or database lookup

MODEL = os.environ.get("CLAUDE_MODEL", "claude-sonnet-5")
client = anthropic.Anthropic()  # reads ANTHROPIC_API_KEY; never a key in code


def outline_report(client_id: str, task: str) -> str:
    """client_id comes from the authenticated session, never from the prompt text."""
    rules = rules_for_client(client_id)
    response = client.messages.create(
        model=MODEL,
        max_tokens=1500,
        system=rules.default + "\n" + rules.client_specific,
        messages=[{"role": "user", "content": task}],
    )
    if response.stop_reason == "refusal":
        raise RuntimeError("The model declined this request; route it to a person.")
    if response.stop_reason == "max_tokens":
        raise RuntimeError("The outline was cut off; raise max_tokens or ask for less.")
    return "".join(block.text for block in response.content if block.type == "text")
Not supplied in this example — defaults and disabled features
Setting not supplied or not visible Meaning for this example
context_management — not supplied here No server compaction strategy is enabled by this body. Host/manual compaction is separate.
output_config — not supplied here No output_config schema or effort override is supplied. A prompt asking for JSON does not provide schema enforcement.
tools — not supplied here No tools are offered through this field.
tool_choice — not supplied here With tools supplied, the default is auto: the model chooses whether to request a tool. Without tools, no supplied tool can be selected.
stop_sequences — not supplied here No custom stop strings are configured. Generation can still stop for end_turn, tool_use, max_tokens, or another documented reason.
stream — not supplied here For a direct non-streaming create/parse call, the result is one message rather than an event stream. A stream helper configures this separately.
thinking — not supplied here No explicit thinking override is shown. Behavior depends on the selected model; omission does not universally mean thinking is off.
cache_control — not supplied here No top-level automatic cache marker is supplied. Check nested system/tool/content blocks for separate cache_control markers.
metadata — not supplied here No application user reference is supplied through metadata. Authentication still comes from the request credentials.
betas — not explicitly supplied No beta list appears in these calls. SDK/client configuration can add headers; inspect those before assuming no beta header is sent.

Full request catalog and omission rules. JSON has no comments; these notes are outside the payload.

Where it lives: app/research/outline.py (the call) + app/config.py (the per-client rules).

Here your application code combines the instructions itself, in the top-level system field. It could first read a trusted Markdown file on purpose with Path(...).read_text(). Even then, the API receives the text; it does not find the file. This example has no automatic hierarchy. It has no tools, because this request only asks for an outline; finding and checking sources would be separate work. The same is true for API services that run in the cloud. Where you deploy does not create a Markdown loader.

Why the other choices felt plausible

There are three other options besides a nested client folder. Each has a real idea in it, and each has a limit.

A client brief in the user message. Sending the client's rules in the user message works. It fits a custom API service or a one-time request well; the custom-API route above does exactly this. Its limit is reuse: you must send the brief again every time. And a Markdown file is not more “enforced” than a message. Both are instructions that the model reads and interprets.

A command-line flag that loads a file. Loading extra instructions at start-up sounds reasonable. But --append-config is not a documented Claude Code feature, and a flag that looks real is not proof that it exists. Claude Code does document --append-system-prompt-file, which adds a file to the system prompt. That is a different mechanism from the hierarchy of project instruction files. CLI reference↗.

An if-sentence in the root file. “If the client is Acme, use APA 7” can guide the model. But it is not configuration logic that runs, and it puts client details into the shared root file. Scoping by folder gives a cleaner split. It is still not the only possible design.

Remember the difference: “coordinator” tells you the agent's job. “Claude Code / Agent SDK / custom API harness” tells you who loads the instructions. Guidance versus enforcement.

Scope: these files are loaded by the agent runtime

This domain is about Claude Code configuration. A custom refund agent that you build on the Messages API does not get this file-loading behavior automatically. A deployed application built on Claude Code or the Agent SDK may use it; that depends on how the runtime is configured. Compare runtimes, explicit file loading, and enforced rules. In every runtime, written instructions (guidance) are different from permission checks that code enforces.

Permissions & settings precedence

JSON
{
  "permissions": {
    "allow": ["Bash(npm run test:*)"],
    "deny": ["Read(~/.zshrc)", "Read(.env*)"]
  }
}

Real file: cookbook:skills/.claude/settings.json — a real project settings file (it uses the old hook format; see the note below).

The layers as real files — annotated, clickable

💡 Everything above describes the layers. This section lets you open them. The </> chips go to the claude-cookbooks repo (a real example of the hierarchy — it really has nested memory and a .claude/ toolbox).

Memory (CLAUDE.md) — which instructions are loaded

Enterprise memory — for the whole organization, installed by IT through MDM (device management), read-only for users (a static example — most personal machines have none, and you can check: ls "/Library/Application Support/ClaudeCode/")

MARKDOWN
# Acme Corp — engineering-wide Claude instructions   <!-- IT deploys this file; devs can't edit it -->
- Never paste customer PII into prompts; reference records by internal ID only.
- All generated code follows the Acme secure-coding checklist (wiki/SEC-101).
- Outbound network calls in examples must use the corporate proxy hostnames.
OS Enterprise files live at
macOS /Library/Application Support/ClaudeCode/ (CLAUDE.md, managed-settings.json)
Linux /etc/claude-code/
Windows C:\ProgramData\ClaudeCode\

Real file: no public example — an organization's policy never lives in a public repo.

~/.claude/CLAUDE.md — user memory · personal · every project you open (guidance)

MARKDOWN
# My global instructions            <!-- travels with YOU, not the repo -->
- Prefer uv for Python; never install a duplicate runtime.
- All web UIs default to the zen-light theme.

Real file: no public example — user memory is always personal.

<repo>/CLAUDE.md — project memory · committed · every teammate's session (guidance)

MARKDOWN
# acme-billing-agent                <!-- travels with the REPO -->
- Run tests with `npm run test:unit` (never plain `npm test` — it hits staging).
- Money amounts are integer cents everywhere. No floats. Ever.

Real file: cookbook:CLAUDE.md — the project memory at the cookbook's root.

<repo>/billing/CLAUDE.md — nested memory · loaded when working under billing/

MARKDOWN
# billing/ conventions              <!-- instructions for this subtree; not enforced settings -->
- Ledger writes go through `postEntry()` — direct INSERTs break replayability.

Real file: cookbook:skills/CLAUDE.md — nested memory for one folder tree · cookbook:managed_agents/slack/CLAUDE.md — memory for one sub-project; there are five of them in one repo.

Settings — the same key at three layers, and who wins

JSONC
// /Library/Application Support/ClaudeCode/managed-settings.json — MANAGED POLICY (static example)
// Deployed by IT via MDM. WINS OVER EVERYTHING below — users cannot loosen it, only tighten.
{
  "permissions": {
    "deny": ["Read(.env*)", "Read(**/*.pem)",        // secrets unreadable org-wide
             "Bash(curl:*)", "Bash(wget:*)"],        // no ad-hoc exfil channels
    "ask":  ["Bash(git push:*)"]
  },
  "forceLoginMethod": "console",                     // org API billing — no personal claude.ai logins
  "forceLoginOrgUUID": "3f1a…-org-uuid",             // and only THIS org
  "disableBypassPermissionsMode": "disable",         // --dangerously-skip-permissions is dead here
  "env": { "HTTPS_PROXY": "http://proxy.acme.internal:8443" }
}

Real file: no public example — a managed policy is always private to the organization.

BASH
# CLI flags — one rung below managed policy, above every settings file (per invocation)
claude -p "run the release checks" \
  --model claude-opus-5 \
  --allowedTools "Bash(npm run test:*)"   # adds preapproval for this run; does not override a deny
JSONC
// ~/.claude/settings.json — USER: yours, every project
{
  "model": "claude-sonnet-5",                    // ← your personal default…
  "permissions": { "allow": ["Bash(git status)"] },
  "hooks": {                                      // deterministic control — fires regardless of prompts
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "./guard.sh" }]
    }]
  }
}
🏭 Production shape: a user-level guard hook

./guard.sh is a relative path, so it would resolve against each project's folder and break in most of them. A user-level hook points to a fixed path in ~/.claude/hooks/. The script reads the pending Bash call from standard input. PreToolUse exit code 2 stops the call, and Claude Code sends stderr to Claude.

JSON
{
  "model": "claude-sonnet-5",
  "permissions": { "allow": ["Bash(git status)"] },
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bash \"$HOME/.claude/hooks/guard.sh\"" }]
    }]
  }
}
BASH
#!/usr/bin/env bash
# ~/.claude/hooks/guard.sh — PreToolUse hook for the Bash tool; event JSON on stdin
set -euo pipefail

cmd=$(jq -r '.tool_input.command // empty')

# Shapes a deny pattern misses: they can sit anywhere inside a compound command.
risky='rm -rf (/|~)|git push (--force|-f)|curl [^|]*\| *(ba)?sh'
if grep -Eq "$risky" <<<"$cmd"; then
  echo "guard.sh blocked: this command matches a risky pattern. Ask the user to run it by hand." >&2
  exit 2                                    # the Bash call does not run
fi
exit 0                                      # everything else follows the normal permission rules

Where it lives: ~/.claude/settings.json + ~/.claude/hooks/guard.sh (personal, every project). The machine needs jq.

Real file: no public example — user settings are always personal.

JSONC
// <repo>/.claude/settings.json — PROJECT: committed, shared with every teammate
{
  "model": "claude-opus-5",                      // …overridden here for this repo…
  "permissions": { "allow": ["Bash(npm run test:*)"] }
}

Real file: cookbook:skills/.claude/settings.json — a real project settings file (an example with outdated keys; see below).

JSONC
// <repo>/.claude/settings.local.json — LOCAL: personal, gitignored, wins over both
{
  "model": "claude-haiku-4-5"                    // …and THIS one wins (managed policy & CLI flags aside)
}

Real file: cookbook:claude_agent_sdk/chief_of_staff_agent/.claude/settings.local.json — a .local file that was committed. Notice the bad pattern: this layer is designed to stay out of git, but here it is in git, and everyone who clones the repo gets it.

The chain, strongest first: managed policy → CLI flags → settings.local.json → project settings.json → user settings.json. This is the table above, now shown with real files.

🪤 An example that is out of date — read it carefully: cookbook:skills/.claude/settings.json is a real settings file, but its format is old or non-standard. contextFiles and projectInfo are not Claude Code settings keys, and its hooks do not have the matcher/hooks nesting shown in the settings file examples. Good exam practice: find what a validator would reject. The rest of the .claude/ toolbox there is standard: cookbook:.claude/commands/review-pr.md — a slash command · cookbook:.claude/agents/code-reviewer.md — a subagent definition · cookbook:.claude/skills/cookbook-audit/SKILL.md — a skill.

At a glance

Surface File / mechanism Scope Open a real one
Project memory CLAUDE.md at repo root (+ nested per-directory) guidance; shared with the team through git cookbook:CLAUDE.md · nested cookbook:skills/CLAUDE.md
User memory ~/.claude/CLAUDE.md guidance; personal, all projects no public specimen — personal by definition
Skills .claude/skills/<name>/SKILL.md (project) · ~/.claude/skills/ (user) expertise used on demand; the model chooses it and Claude Code runs it, or you type /name cookbook:.claude/skills/cookbook-audit/SKILL.md
Slash commands .claude/commands/<name>.md reusable prompt templates that the user starts cookbook:.claude/commands/review-pr.md
Subagents .claude/agents/<name>.md limited workers with their own prompt and tools cookbook:.claude/agents/code-reviewer.md
Hooks settings.json hooks (PreToolUse, PostToolUse, Stop, …) enforcement: shell commands that run on events, every time cookbook:skills/.claude/settings.json (legacy shape)
Settings & permissions .claude/settings.json (shared) · settings.local.json (personal) · ~/.claude/settings.json (user) enforcement: allow/deny tools; also env and model cookbook:skills/.claude/settings.json · cookbook:claude_agent_sdk/chief_of_staff_agent/.claude/settings.local.json
MCP servers .mcp.json (project) · user/local config tools and resources from outside the cookbook ships no .mcp.json — sample on Samples
Headless claude -p "…" + --output-format json/stream-json CI/CD, scripts — deployed examples on Deployment & Operations —

💡 The last column lets you open each layer, not only read about it: every chip is a real file. Public chips link to the claude-cookbooks repo at a fixed commit.

Skills vs slash commands vs subagents — the discriminator

How the current product separates them: A user can start a skill by typing /name, or the model can select it, depending on how the skill is configured. Custom commands and skills now use the same invocation system. Older .claude/commands/ files still work. So “skill” and “slash command” can be the same thing. A subagent is different: it is a separate context where work runs. Official extension guide↗.

These three mechanisms, and the exact lines between them, are correct as of mid-2026 — check current docs. Claude Code's configuration options keep changing.

Skill Slash command Subagent
Chosen by (the decision) the model, when your request matches the skill’s description — or you, with /name the user types /name; the model may also select the custom skill behind it, if that is enabled the model hands off work, or the user asks
Run by (the execution) Claude Code — loads SKILL.md, then runs the requested tool calls that are permitted Claude Code — fills in the template Claude Code — starts a new, empty context window
Body is instructions + optional scripts and files (SKILL.md + folder) one prompt template (supports $ARGUMENTS) a system prompt + tool rules, running in its own context window
Best for reusable expertise and procedures ("how we do X") prompts you use often, with parameters heavy work kept apart (review, search), so it does not fill the main context

🧭 What the official words mean: The docs and the exam say skills are “model-invoked”. That means the model chooses the skill. It reads each skill’s description and decides that one fits, exactly as it picks a tool from tools[]. It never means that the model runs it. The model cannot act by itself here either. It selects, and Claude Code (the harness in your terminal) performs the call. This is the same split of work as the tool-use handshake. Hooks are the other end: nobody chooses them at run time. The harness runs them on events, the same way every time.

🪤 Trap: "The team keeps pasting the same 20-line release-checklist prompt" → slash command. "The team wants Claude to know the deployment procedure whenever deploys come up" → skill. "Code review uses up the main session's context" → subagent.

Who invokes a skill and who runs it?

When you type /go-live, which file wins?

You normally type /go-live, not /skill go-live or /command go-live. The name after / is the command name. Claude Code looks up that name among the loaded definitions. The model does not guess which file wins.

TEXT
.claude/skills/go-live/SKILL.md   → /go-live (skill)
.claude/commands/go-live.md       → /go-live (legacy command)

If both define go-live: the SKILL wins.
If only the legacy command exists: that command runs.

/go-live production
 └ name   └ argument text passed to the selected procedure

The slash is only the way you call something. It is not a separate kind of stored file. When enterprise, personal and project skills have the same name, the order is enterprise > personal > project. Plugin skills use /plugin-name:skill-name. These rules only decide which name wins; the rules about who may invoke still apply. If you want both procedures available, give them different names.

See Claude Code’s name-conflict rules↗.

Who invokes it? Follow the request through three owners

You give the goal. Either you name a skill directly with /name, or the model picks an available skill that fits your words. Claude Code (the application around the model) loads the skill. The model then follows its instructions and asks for tools. Claude Code runs the tool calls that are permitted and returns the results. A skill is a set of instructions. It is not a Python thread and not a deployment service.

TEXT
YOU: “Review my changes.”
  ↓
CLAUDE: selects the review-changes skill
  ↓
CLAUDE CODE: loads its SKILL.md instructions
  ↓
CLAUDE: requests a tool to read the diff
  ↓
CLAUDE CODE: checks permissions and runs that tool
  ↓
CLAUDE: reads the result and explains the review

This sequence explains the flow. It is not a raw Messages API payload. The Skill tool and slash-command handling are part of Claude Code. A custom app built on messages.create must build or add its own way to load skills.

Four situations: direct invocation, automatic selection, suggestion, execution

Situation What happens
You type /review-changes You called the skill directly. Claude Code loads it, and the model works from its instructions.
You type “Review my changes” The model can pick a matching skill if model invocation is turned on. Matching normal language is the model's decision. It is not a guaranteed exact-keyword trigger.
Claude says “You could use /go-live” That is only a suggestion. Text that mentions a command does not prove that a skill ran or that a deployment happened.
Claude requests a deployment tool/command Claude Code handles the request under the permissions that apply. The tool result and a deployment check show what happened. A skill name or a confident sentence does not.

Full file: both user and model may invoke this skill

Illustrative .claude/skills/review-changes/SKILL.md. Neither invocation-control field is set, so the default behavior applies. This file only illustrates the example; it is not an installed skill.

MARKDOWN
---
name: review-changes
description: Review uncommitted code changes when the user asks for a review or asks what changed.
---
Read the current diff using an available permitted tool.
Explain what changed and identify specific defects with file locations.
Do not modify files.

You can type /review-changes. You can also ask “Are there problems in my changes?”, and the model may pick it. The description helps the model pick it. The body explains the work after it loads.

Full file: a deployment skill invoked only by the user

Illustrative .claude/skills/go-live/SKILL.md for a team that wants only an explicit slash command to start it. This is a different configuration. It is not a change to this repository’s real deployment skill.

MARKDOWN
---
name: go-live
description: Deploy the reviewed application to production.
disable-model-invocation: true
---
Use the project’s documented deployment procedure.
Check the selected environment and required approvals before deployment.
Verify the deployment result and report failures accurately.

Type /go-live to start this version. The model cannot start this skill by itself from a normal “go live” sentence. It can suggest that you type /go-live. This setting controls who can start the skill. Permission to really deploy is still decided by the runtime and the deployment system.

Invocation switches: read the flag literally

Situation What happens
Both fields omitted The user can type /name, and the model can pick the skill when it fits.
disable-model-invocation: true Turns off invocation by the MODEL. The user can still type /name.
user-invocable: false Turns off the USER’s slash command. The model can still pick the skill.
Neither flag controls shell execution Tool permissions and deployment credentials still control the real commands. A description that matches a request is not a permission check.

Your “go live” example: what caused it to start?

Suppose the description says “Use when asked to ship, deploy, or go live”, and model invocation is on. Your sentence gives the intent, and the model chooses the procedure. So both of you take part: the user asked for the result, and the model picked the matching skill. With /go-live, you choose the procedure directly. With disable-model-invocation: true, you must use the slash command.

context: fork changes where the instructions run: inside a separate subagent context. It does not decide whether the user or the model picks the skill. Putting scripts next to SKILL.md does not run them. A script runs only through a defined mechanism, such as a tool call or a command expansion that you configured explicitly.

Are slash commands and skills different things?

A custom /name can be the way into a skill. Current Claude Code merges custom command templates and skills; older .claude/commands/name.md files still work. Do not conclude “only the user can start it” just from the words slash command. Check the invocation settings of the custom skill behind it. Built-in commands such as /help are a separate group. Do not assume every slash command is a skill that the model can pick.

Remember: USER gives the goal → USER or MODEL picks the skill → CLAUDE CODE loads it → MODEL asks for actions → CLAUDE CODE runs the permitted tools.

Read the current Claude Code rules for starting skills↗. The same worked examples are in the skill glossary dialog and the slash command glossary dialog.

Skill frontmatter — the three options the exam names

SKILL.md files start with YAML frontmatter. This is the config block that the harness (Claude Code) reads. The body below it is what the model reads (open the term for all the parts). These fields affect where the skill runs, which tools are pre-approved, and the hint shown for the command:

Frontmatter What it does Exam trigger phrase
context: fork runs the skill in a separate sub-agent context. To choose WHICH agent, add a second key on its own line: agent: general-purpose under context: fork (YAML: one key: value per line). Claude Code looks up the name in the agent registry: built-ins like general-purpose, YOUR definitions in .claude/agents/*.md (frontmatter config + body used as system prompt), and plugin agents — where agent names come from. So long output never fills the main conversation. Despite the name, it is not a fork of your conversation: the subagent gets the skill content as its prompt and does not see your conversation history. It runs in the background by default (background: false makes Claude wait). The other choice is to leave it out: without context, the body loads inline into the current conversation. That is the default "the skill's output floods the session"
allowed-tools pre-approves the listed tools. It does not remove tools that are not listed. For enforced read-only access, use a subagent's tools list, deny rules or a hook "the skill can use these tools without per-use approval"
argument-hint shows a hint for the argument when you type the command. It does not check or require any argument "developers keep invoking /deploy without saying which environment"

Project skills live in .claude/skills/ (shared through git). Personal versions go in ~/.claude/skills/ with a different name, so they do not hide the team's version.

Skill or agent? — packaging a procedure like “go live”

The table above separates the mechanisms. Here is the question you really face: my team has a stage-it / ship-it / go-live procedure — should it be a skill or an agent? The answer is to combine them, not to choose one:

Concern Owner
The procedure — steps, house rules, “paste only the final URLs back” the skill (SKILL.md body — easy to move, shared in git, picked explicitly or by matching its description, within the invocation controls)
The mechanics — the exact commands, their order, safety checks a script that the skill calls (scripts/ship.sh) — runs the same way every time, and you can test it without any model
The isolation — build output never reaches your session context: fork on the skill
The executor — which role and tool rules run it agent: on the skill (a built-in, or your own .claude/agents/ definition with fewer tools)
MARKDOWN
---
name: go-live
description: Use when asked to ship, deploy to production, or "go live".
context: fork                 # the WHOLE procedure runs in a disposable context…
agent: general-purpose        # …executed by this agent (or a separately configured deploy agent)
allowed-tools: Bash, Read
---
1. Run `scripts/ship.sh "<message>"` and wait for it to finish.
2. Verify the live URL returns 200.
3. Report back ONLY: commit shas, deploy URL, and any failure verbatim.

In this example, model invocation is on (the flag that turns it off is missing). So saying “ok, go live” can lead to this: the model matches the description → Claude Code starts the forked agent → the checklist and all build output stay inside that context and end there → your session gets three lines. This repo now has exactly this skill — its frontmatter in git currently leaves out disable-model-invocation. So the model may pick it from a matching request, and /go-live picks it explicitly. Session configuration and permissions still apply to that choice. The bad pattern is to put the procedure inside a deploy agent's system prompt instead. It works through delegation, but the knowledge is locked inside the executor. The main loop cannot see it, other agents cannot share it, and it starts to differ from the team's skill the first time either one is edited.

🧭 This is the same split between knowledge and executor that you already know. CLAUDE.md and skills hold what to do. Agents hold who does it, where, and with which tools. Combine them; never copy one into the other.

Go live: where permissions are stored

Open the complete permission walkthrough in a glossary dialog.

Go live: three permission checks, not one permission inside the skill

Imagine /go-live asks Claude Code to start a GitHub workflow. The workflow deploys an existing container image to a staging Cloud Run service. The skill only describes the procedure (guidance). Three separate systems enforce permissions. Claude Code checks local tool access. GitHub checks the caller. GCP checks the workflow's identity. Passing one check does not give the next permission. This is a design for teaching; it does not describe this repository’s real ship script.

Show complete text example
TEXT
You: /go-live
  → Claude reads the skill and requests Bash
  → Claude Code checks its tool permission settings
  → gh sends a workflow dispatch using its authenticated identity
  → GitHub checks repository/workflow access
  → GitHub applies staging environment protection rules
  → Runner obtains short-lived GCP credentials through OIDC federation
  → gcloud sends the deployment request
  → GCP IAM checks the deployment identity’s resource permissions
  → Workflow reports success or failure; Claude reads that result

Where each setting lives—and who enforces it

Location or signal What it means
Skill procedure Repository .claude/skills/go-live/SKILL.md: instructions, plus optional fields for invocation and tool approval (guidance). No GitHub token or GCP key belongs in this text.
Claude Code tool rules User ~/.claude/settings.json; shared project .claude/settings.json; personal project .claude/settings.local.json; organization-managed settings where configured. Claude Code combines the configuration that applies and enforces the tool rules.
Permission for the local GitHub caller Access to the GitHub repository or organization, plus the credential that gh uses. gh auth login normally uses the operating system's credential store when one exists; a token in an environment variable can also log you in. The token says who the caller is; GitHub checks what that caller may do. Do not print the token or copy it into the prompt.
Workflow token permissions The permissions block in .github/workflows/deploy-staging.yml sets what the workflow’s GitHub token can do. This is separate from the identity that started the workflow.
Staging approval rules GitHub repository Settings → Environments → staging. Set reviewers and the branches or tags allowed to deploy, where supported. Writing environment: staging in YAML only selects the environment. It does not create a reviewer rule by itself.
GitHub-to-GCP trust The GCP IAM Workload Identity Pool and provider with their conditions, plus the right to act as the chosen service account. Limit this trust to the right GitHub organization and repository, and to the right workflow, ref, and environment claims.
GCP resource permissions IAM role bindings stored in GCP on the project, resources, and service accounts. Terraform may manage them, but GCP enforces the policy that is applied. A service-account email in YAML is only a name. It does not grant anything.
Runtime application identity The runtime service account of the Cloud Run service is separate from the workflow’s deployer service account. After deployment, the application's own access should have its own permissions.

1 · Full illustrative skill file: request the workflow, then inspect its outcome

Example .claude/skills/go-live/SKILL.md. Only the user can start this example. It leaves out allowed-tools, so the skill adds no pre-approval of its own. The normal session permission rules still apply.

Show complete markdown example
MARKDOWN
---
name: go-live
description: Start and verify the staging deployment workflow.
disable-model-invocation: true
---
1. Confirm the target repository and reviewed release ref with the user’s request.
2. Request the command:
   gh workflow run deploy-staging.yml --repo EXAMPLE_ORG/EXAMPLE_REPO --ref main
3. Locate the workflow run for this dispatch; inspect its status and logs.
4. Report success only after the deployment and health verification succeed.
5. If a permission check fails, report which system refused it. Do not change IAM to bypass the failure.

These are instructions for the model (guidance). Claude Code runs the commands the model requests. Stronger words in the prompt cannot fix a missing credential or a failed IAM check. Reading this example does not deploy anything.

2 · Full illustrative Claude Code settings: permission to request a shell action

Example .claude/settings.json. This configuration is for teaching; it does not change your real settings. The rules are examples for single commands, not a complete sandbox policy.

Show complete json example
JSON
{
  "permissions": {
    "allow": [
      "Bash(git status)"
    ],
    "ask": [
      "Bash(gh workflow run *)",
      "Bash(gcloud run deploy *)"
    ]
  }
}

In a Claude Code session that uses these rules, matching deployment commands need approval first. The rules hold command patterns, not credentials. A skill’s allowed-tools can pre-approve the listed tools, but deny and ask rules still come first. Allowing Bash does not log gh in, does not give a GitHub role, and does not create a GCP IAM binding. When a script wraps commands, check the wrapper too, and also use sandbox and cloud controls. A text pattern is not a complete security boundary.

3 · Full illustrative GitHub workflow: use an already-built image

Example .github/workflows/deploy-staging.yml. It needs GitHub and GCP setup first, plus the environment variables described below. Pasting it alone will not deploy anything. The action tags show major versions so they are easy to read; production workflows should pin reviewed commit SHAs.

Show complete yaml example
YAML
name: Deploy staging
on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: staging
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: actions/checkout@v4
      - name: Obtain a short-lived GCP identity
        uses: google-github-actions/auth@v3
        with:
          workload_identity_provider: ${{ vars.GCP_WIF_PROVIDER }}
          service_account: ${{ vars.GCP_DEPLOYER_SA }}
      - uses: google-github-actions/setup-gcloud@v2
      - name: Deploy the approved image to staging
        env:
          PROJECT_ID: ${{ vars.GCP_PROJECT_ID }}
          IMAGE: ${{ vars.STAGING_IMAGE_DIGEST }}
          RUNTIME_SA: ${{ vars.GCP_RUNTIME_SA }}
        run: |
          gcloud run deploy orders-staging \
            --project="$PROJECT_ID" \
            --region=us-central1 \
            --image="$IMAGE" \
            --service-account="$RUNTIME_SA" \
            --quiet

Store these non-secret IDs as variables of the GitHub staging environment: the provider resource name, the deployer service-account email, the project ID, the fixed image digest, and the runtime service-account email. OIDC federation gives the runner short-lived credentials, so no long-lived GCP key is stored here. id-token: write lets the job ask for a GitHub identity token. It gives no Cloud Run permission. This example stops at deployment. Add health checks for your application before you treat the release as verified.

4 · What must an administrator configure in GCP first?

For the design above, where the workflow acts as a service account, the administrator must set up a trusted GitHub OIDC provider. Only the intended workflow identity may act as the deployer service account. Give that deployer the permissions the deployment needs, such as Cloud Run Developer, and Service Account User on the specific runtime service account. Access to the image, and any registry permissions across projects, must also be set up. The smallest set of roles depends on the job: building an image, creating resources, or only updating an existing service.

These grants live in GCP IAM. The skill does not create them. Neither does the service_account YAML field, or id-token: write. An administrator or platform team creates the trust and the grants. The normal deploy workflow only uses the existing grants. It should not give itself wider roles.

5 · Which system rejected the operation?

Location or signal What it means
Claude Code asks before Bash The current settings need local tool approval (Claude Code). GitHub has not received the request yet.
gh reports not logged in or GitHub denies dispatch Check the CLI identity, repository access and credential permissions (GitHub). The local Bash approval already passed.
Workflow waits for staging review GitHub environment protection is holding the job. This is not a Claude Code approval prompt.
GCP federation/authentication fails Check the provider trust conditions and the setup for acting as the service account (GCP). GCP does not trust a GitHub identity automatically.
gcloud returns a permission-denied deployment error Check the logged-in deployer and the IAM permissions the resource needs (GCP). Login (authentication) can succeed while the permission check (authorization) fails.
Workflow dispatch succeeded That only means the workflow started. Read its final result and the health check before you claim the deployment worked.

What if the skill runs gcloud directly on your laptop?

Then GitHub Actions is not part of that path. Claude Code still checks whether it may run gcloud. The CLI uses the account that is logged in on your machine, or a service account it acts as. GCP checks that identity’s IAM roles. The gcloud CLI login and the Application Default Credentials that SDKs use are two separate configurations. Your laptop login does not move to a GitHub runner automatically.

Recall: SKILL = procedure (guidance). CLAUDE CODE SETTINGS = permission to use local tools (enforced by Claude Code). CREDENTIAL = who is calling. GITHUB/GCP POLICY = what that identity may do (enforced by GitHub and GCP).

References: Claude Code permission rules↗, GitHub OIDC to GCP↗, GCP deployment pipeline federation↗.

CosmicKeys: a real go-live permission trace

Open the full CosmicKeys case inside the glossary.

CosmicKeys: the real go-live flow, traced from repository files

Sources read: the CosmicKeys skill, deploy-prod.yaml, the Terraform bootstrap, and the backend route registration. This shows what the files in git intend. It is not a checked live deployment, and not an audit of live IAM. No workflow was started. The frontend and the backend deploy in different ways.

Show source excerpt or execution flow
TEXT
stage-it → push staging → frontend preview on Vercel
ship-it → merge staging into main → push main
  → GitHub Actions deploy-prod.yaml (matching path changes)
  → build image → deploy green at 0% traffic in US / EU / Asia
go-live → wait for the correct successful green deployment
  → LOCAL gcloud update-traffic → green gets 100% in each region
  → frontend promotion through Vercel CLI when needed
  → verify backend and frontend health

go-live also has a step that merges staging into main and pushes it, which overlaps with ship-it. These are reusable procedures (skills), not three new kinds of agent. Its frontmatter leaves out disable-model-invocation, so by default the model can pick it from normal language. /go-live is the explicit way.

The important distinction: there are TWO GCP callers

Part Explanation
GitHub Actions runner Uses google-github-actions/auth with a federated deployment service account. It builds and pushes the image and runs gcloud run deploy --no-traffic --tag=green.
Local Claude Code tool execution Runs gcloud run services update-traffic from the user’s own machine. It uses that machine's gcloud credentials or configured impersonation — not the GitHub runner’s temporary token.
Vercel CLI Uses its own logged-in Vercel identity and team access to promote the frontend. Neither GitHub WIF nor a GCP role gives this access.

Actual workflow trigger and authentication wiring

A short excerpt from .github/workflows/deploy-prod.yaml. The build job and the deploy job both declare the GitHub permissions and login step shown here. Only secret NAMES are shown; the values were not read.

Show source excerpt or execution flow
YAML
on:
  push:
    branches: [main]
    paths:
      - 'backend/**'
      - '.github/workflows/deploy-prod.yaml'
  workflow_dispatch:
    inputs:
      skip_build:
        type: boolean
        default: false

# Inside each relevant job:
permissions:
  contents: read
  id-token: write
steps:
  - uses: google-github-actions/auth@v2
    with:
      workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
      service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

A matching push starts the normal path; workflow_dispatch also allows manual runs. The job gets an OIDC identity and exchanges it through GCP federation. The secret entries hold a provider ID and a service-account ID, not a stored GCP private key. This file does not select a GitHub environment with environment: staging/production. So this workflow does not create the environment approval queue from the earlier illustrative example.

Where CosmicKeys declares the GCP permissions

A short declaration from terraform/bootstrap/main.tf. Terraform describes the policy you want; GCP enforces it after it is applied. This review did not check the current live bindings.

Show source excerpt or execution flow
HCL
# Federation permits this repository identity to impersonate the deployer.
resource "google_service_account_iam_member" "github_actions_wif" {
  service_account_id = google_service_account.github_actions.name
  role = "roles/iam.workloadIdentityUser"
  member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.github_pool.name}/attribute.repository/${var.github_org}/${var.github_repo}"
}

# Existing repository declaration: broad project-level authority.
resource "google_project_iam_member" "github_actions_owner" {
  project = var.project_id
  role = "roles/owner"
  member = "serviceAccount:${google_service_account.github_actions.email}"
}

The workload identity provider checks the repository owner. The impersonation binding narrows access to the named repository. The Owner grant is very broad. It is not a template for giving only the smallest needed rights. For a narrower design, separate infrastructure administration from normal deployment, and grant only the actions that are needed. A trust rule answers “who may become this identity?”. The project role answers “what may that identity do?”

The local traffic promotion command and its separate permission check

The real command format in go-live. It is shown for learning; it was not run. Matching commands target europe-west1 and asia-southeast1.

Show source excerpt or execution flow
BASH
gcloud run services update-traffic cosmickeys-production-us-api \
  --region=us-central1 --project=cosmickeys --to-tags=green=100

Claude Code decides whether the local Bash tool may run. Then GCP checks whether the active gcloud identity may update the service. The project flag chooses the target project; it does not give access. The green tag chooses a deployed revision; moving traffic does not build a new image. The skill itself holds neither the gcloud login nor an IAM role.

What to verify before using this as a production template

Part Explanation
Match the release The skill checks the latest workflow run again and again. A safer controller first links the pushed commit SHA to the successful run and its green revision, and only then moves traffic. The “latest successful run” may belong to a different release.
Scope detection The skill sorts files one way and the workflow's push paths use another. The workflow starts by itself for backend/** or its own YAML. Some paths that the skill calls backend do not match those filters by themselves.
Health checks The backend code registers /api/health, and go-live uses that path. The workflow checks /health, and after failed retries it ends with a warning instead of failing. So a successful workflow does not prove that this health check passed.
Rollback target The skill’s rollback snippet uses --to-latest. Latest is not always the previous good revision. For a planned rollback, record the last healthy revision and the traffic split.
Regional transition Three traffic updates one after another are not one atomic (all-or-nothing) transaction. A later failure can leave regions on different revisions. Record the results and plan the recovery.

Read the evidence: which file answers which question?

Part Explanation
.claude/skills/go-live/SKILL.md Matching the request, choosing the scope, local traffic commands, and steps to promote the frontend.
.claude/skills/stage-it/SKILL.md and ship-it/SKILL.md Steps to push to staging and to push to the main branch.
.github/workflows/deploy-prod.yaml Trigger filters, runner login, image build, and green deployment in each region.
terraform/bootstrap/main.tf Federation provider, deployer account, impersonation binding, and the project Owner declaration.
backend/app/main.py Health router registered with the /api prefix.

Credential values, live IAM grants, branch protections, and the active local gcloud identity were not read. The source configuration explains the design. It does not prove that the current account has access.

.claude/rules/ — path-scoped conventions with glob frontmatter

When do rules load? .claude/rules/*.md belongs to the MEMORY family (guidance). Plain topic files load when the session starts, exactly like CLAUDE.md content. Splitting them into files only keeps things tidy; it does not delay loading. Adding paths: frontmatter is what makes a file conditional: it appears only when the files being worked on match the glob. So: split only for tidiness → always loaded. Split with paths: → loaded when relevant.

🧭 “Keep these standing rules” — where do they go? When you tell a session “from now on, always X”, that is a rule (memory), not a skill. The assistant writes it into ~/.claude/CLAUDE.md (global, every project) or into the repo's CLAUDE.md / .claude/rules/ (whole project). Then the HARNESS loads it in every session. Nothing chooses it; it is always in context. A skill works the opposite way: it is a procedure used on demand, and the model CHOOSES it by its description when it fits. A limit you must never forget → rule. A procedure you run sometimes → skill.

One huge CLAUDE.md loads everything into every session. Two mechanisms make that smaller:

The difference the exam tests (a pattern from the sample questions): Conventions for a file type found all over the tree (test files everywhere, React components in many folders) → glob-pattern rules. A folder-level CLAUDE.md is tied to one folder and cannot follow a pattern. Conventions for one folder and its subfolders → a nested CLAUDE.md works. Relying on Claude to "work out which section applies" from one big file → always the wrong answer when it is offered.

Real files: the cookbook has no .claude/rules/. The nested cookbook:skills/CLAUDE.md is its only conditional layer.

Watch the merge happen — layered config, git-diff style

Two families of files combine in two different ways. This picture helps you remember precedence. Left: each layer's file (weakest first). Each card's open → chip is a real file: the cookbook example. Right: the effective config as it builds up. Each line shows the layer that won; values that lost are crossed out.

flowchart TB
  subgraph SETTINGS [settings — per-key OVERRIDE, strongest wins]
    U1[user settings] --> M1{merge by key}
    P1[project settings] --> M1
    L1[settings.local.json] --> M1
    MP1[managed policy] -->|always wins| M1
    M1 --> E1[ONE effective value per key]
  end
  subgraph MEMORY [memory — UNION, all load together]
    E2[enterprise CLAUDE.md] --> M2{concatenate}
    U2[user CLAUDE.md] --> M2
    P2[project CLAUDE.md + rules/] --> M2
    N2[nested CLAUDE.md] -->|specificity wins at conflicts| M2
    M2 --> EF2[ALL lines in context]
  end

Settings (enforced by Claude Code) — each key is overridden separately. Watch model change owner three times, permissions.deny arrive and stay fixed, and the allow list grow:

JSON
{"mode":"override","layers":[
 {"name":"user","file":"~/.claude/settings.json","entries":[
   {"k":"model","v":"claude-sonnet-5"},{"k":"permissions.allow[git]","v":"Bash(git status)"},{"k":"hooks.PreToolUse","v":"guard.sh"}],
  "note":"yours, every project — the weakest layer"},
 {"name":"project","file":"<repo>/.claude/settings.json","pub":"cookbook:skills/.claude/settings.json","entries":[
   {"k":"model","v":"claude-opus-5"},{"k":"permissions.allow[test]","v":"Bash(npm run test:*)"}],
  "note":"committed — every teammate inherits"},
 {"name":"local","file":"<repo>/.claude/settings.local.json","pub":"cookbook:claude_agent_sdk/chief_of_staff_agent/.claude/settings.local.json","entries":[
   {"k":"model","v":"claude-haiku-4-5"}],
  "note":"personal, gitignored — beats both below it"},
 {"name":"managed","file":"/Library/…/managed-settings.json","entries":[
   {"k":"permissions.deny[env]","v":"Read(.env*)"},{"k":"disableBypassPermissionsMode","v":"disable"}],
  "note":"IT's layer — nothing below can loosen it"}]}

Memory (guidance) — combined context. The matching instruction files are read together. No fixed “last file wins” algorithm settles conflicting text, so keep the instructions consistent. The picture shows which instructions are present. It does not guarantee enforcement:

JSON
{"mode":"union","layers":[
 {"name":"enterprise","file":"/Library/…/ClaudeCode/CLAUDE.md","entries":[
   {"k":"org","v":"never paste customer PII"}],
  "note":"MDM-deployed, read-only"},
 {"name":"user","file":"~/.claude/CLAUDE.md","entries":[
   {"k":"style","v":"prefer uv for Python"},{"k":"ui","v":"zen-light theme by default"}],
  "note":"travels with YOU"},
 {"name":"project","file":"<repo>/CLAUDE.md","pub":"cookbook:CLAUDE.md","entries":[
   {"k":"tests","v":"npm run test:unit (never plain npm test)"},{"k":"style","v":"THIS repo uses poetry","conflicts":false}],
  "note":"travels with the repo — note style now has TWO lines loaded"},
 {"name":"nested","file":"<repo>/billing/CLAUDE.md","pub":"cookbook:skills/CLAUDE.md","entries":[
   {"k":"billing","v":"ledger writes via postEntry() only"}],
  "note":"joins when working under billing/ — adds local guidance; reconcile conflicting prose"}]}

⚠️ That enterprise line — “never paste customer PII” — is only advice, like all memory. The rule and an accidental paste reach the model in the same context. So the rule cannot prevent the data from being exposed; it can only guide behavior afterwards. Real prevention is code that runs the same way every time, BEFORE the call: the PII boundary.

🧭 The exam difference inside these two pictures: a single-value setting ends up with one value; lists and permissions have their own merge rules (ask “who wins?”). Memory keeps ALL the lines (ask “what is loaded — and which line is more specific?”). Skills and commands have their own rules for names and invocation. Do not guess those rules from how instruction files load.

Beyond the standard mechanisms — the project-map pattern (a house invention)

Everything above sets CONDITIONS on what loads: nested CLAUDE.md (by folder) and paths: rules (by file pattern). This pattern turns the question around. Instead of loading more knowledge under conditions, it always loads a tiny index. The index is a file of about 60 lines: one mermaid diagram of the project's main structure plus a | Need | Location | lookup table. The project launcher (a shell script, not Claude Code itself) adds it to the system prompt when the session starts.

Mechanism What loads When What it buys
one big CLAUDE.md all knowledge always simple; highest token cost
topic files .claude/rules/*.md all knowledge, organized session start easier to maintain
paths: rules one topic's knowledge when the touched files match tokens spent only where they apply
nested CLAUDE.md one folder's knowledge when working in that folder knowledge stays near its code
project map (house) an index, not knowledge session start, ~1–2K tokens replaces the search: “where does X live / how does it flow” is answered with ZERO tool calls

The context numbers show why this is a real pattern, not just a convenience. Without the map, every “where is the deploy script?” costs a search-and-read round trip: 2–6 tool turns. Each turn returns output that enters the context and stays there. Ten questions ≈ thousands of tokens of search output used only once. The map is about 1.5K tokens. It loads once, answers all ten, and keeps exploration read-only. It also survives compaction better than search results: an index compresses well; long find output does not.

🧭 In one line: rules load what the session should KNOW; the map loads what the session can FIND. One gives knowledge up front; the other gives an index up front, so every later lookup is exact. It works together with the standard ladder; it does not replace it. It also needs a refresh skill that rebuilds the map whenever the structure changes (an out-of-date index is worse than none).

Plan mode vs direct execution — and the Explore subagent

Two ways to work in Claude Code, and one rule to choose:

🎯 "Restructure the monolith — where do you start?" → plan mode: explore before changing (not "start editing and let the boundaries appear"). "One bug fix with a stack trace?" → direct execution. "Discovery output is filling the context window?" → Explore subagent.

Explore, then refactor: two subagents in sequence

The task: find every use of the old v1 payments API, then change each one to v2. Two Claude Code subagents can do this: one explores, one edits. The difficult part is not the editing. It is the handoff: the second subagent knows only what it is given.

What each subagent starts with

Facts from the Claude Code subagent documentation↗:

A fresh subagent STARTS with A fresh subagent does NOT get
Its own system prompt: the body of its .md file The main conversation's history
The task message: the delegation prompt the main Claude writes when it hands off the work Files or search results that another agent read
The CLAUDE.md files (the built-in Explore and Plan agents skip them) The other subagent's tool calls or reasoning
A git status snapshot, preloaded skills, a list of the other agents Anything the main Claude decided not to write into the task message

What comes back: only the subagent's final message. Its file reads, searches and intermediate steps stay in its own context. That is the whole point (the main context stays small), and it is also the risk (details stay behind).

The two definition files

The frontmatter (the YAML between --- lines) configures the agent. The body is its system prompt. There is no output-schema field in the frontmatter. The "structured output" you saw in the course is the Output section in the body: an instruction that defines exactly what to return.

MARKDOWN
---
name: v1-usage-explorer
description: Finds every use of the v1 payments API. Use before a v1-to-v2 migration. Does not change source code.
tools: Read, Grep, Glob, Write
model: haiku
---
You find every use of the v1 payments API (PaymentsV1Client, /api/v1/payments, PAYMENTS_V1_* settings).
Never change source files. The only file you write is the inventory below.

## Output (the handoff contract)
1. Write the complete inventory to .claude/handoff/v1-usages.json:
   {"commit": "<git rev-parse HEAD>", "total": <n>,
    "usages": [{"file": "src/billing/Charge.java", "line": 42,
                "symbol": "PaymentsV1Client.charge", "kind": "call|import|config|test",
                "note": "retries on timeout"}]}
2. Your final message: the file path, the total, the count per kind,
   and every place you could not classify. Do not retell the list in prose.
MARKDOWN
---
name: v2-refactorer
description: Migrates v1 payments API uses to v2 from an inventory file. Use after v1-usage-explorer.
tools: Read, Edit, Grep, Glob, Bash
model: sonnet
---
Your task message gives you an inventory file. Read the WHOLE file first.
For each usage, apply the mapping in docs/payments-v2-migration.md. Read the code around a usage before you edit it.
The inventory is a starting point, not proof: before you finish, search again for every v1 symbol
and handle any use the inventory missed. Then run the tests of the modules you changed.

## Output
- Changed files (count and list)
- Inventory items you did NOT change, each with the reason
- Extra uses you found that were not in the inventory
- The test command you ran and its result

Note on enforcement: tools: Write lets the explorer write anywhere. If "writes only the inventory" must be guaranteed, add a permission rule or a PreToolUse hook for that agent. The sentence in the body is guidance only.

Who runs what, and in which order

You ask in the main conversation: "Use v1-usage-explorer to build the inventory, then use v2-refactorer with that inventory file." The docs describe exactly this: subagents used in sequence; each returns its result to the main Claude, which then passes the relevant context to the next one.

Step Who acts What happens
1 Main Claude Delegates to v1-usage-explorer with a task message
2 Explorer (own context) Searches and reads many files; writes v1-usages.json; returns a short final message
3 Main Claude Receives only that final message. Waits for it: the refactor depends on it
4 Main Claude Writes the refactorer's task message: the file path + "use it unchanged"
5 Refactorer (own context) Reads the file, edits, searches again, runs tests; returns its report
6 Main Claude, you, or CI Checks the result: no v1 symbol left, tests green

Sequential, because of the dependency. The refactorer cannot start before the inventory exists. Parallel where the work is independent: several explorers can scan different modules at the same time. After the inventory exists, several refactorers can each take one module's slice, as long as no two of them edit the same file (isolation: worktree in the frontmatter gives each one its own git worktree). Do not run explore and refactor at the same time on the same code.

Where findings get lost: three hops

Hop What can go wrong The fix
1. Explorer's context → its final message It read 300 files but returns "about 40 call sites, mostly in billing". The list stays behind in its context and is gone. An output contract that requires every item (file, line, kind), and a file for anything long
2. Main Claude → the refactorer's task message The main Claude paraphrases what it received: "update the billing calls". Items outside billing disappear. Pass the file path and say "use the inventory unchanged" (or paste it word for word). Never ask for a summary of a summary
3. Refactorer → its edits It trusts a list that may be incomplete, or stale after new commits. It edits only what the list names. The inventory records the commit. The refactorer searches again and runs the tests. A final check (grep for v1 symbols = 0, the test suite) proves completion

Memory anchor: a report is a summary unless you define a contract; a file is the full handoff; a new search is the proof.

Fork, fresh subagent, or no subagent?

Option What it starts with Use it when Watch out
Fresh custom subagent (.claude/agents/*.md) Its own prompt, tools and model + the task message Each role needs its own instructions and tool limits (read-only explorer, editing refactorer) Needs an explicit handoff (above)
Fork (/subtask <task>) The whole conversation so far, with the main session's system prompt, tools and model; shares its prompt cache The next step needs everything already discussed (for example mapping decisions you agreed on in the chat), and you want its noisy work out of the main context It does not use the refactorer's restricted tools or prompt. It also carries all the old noise. A fork cannot start further forks
Built-in Explore Read-only tools; skips CLAUDE.md Quick discovery that may come back as a summary It is one-shot (cannot be resumed). For a complete inventory, use a custom explorer with a contract
No subagent (main conversation does both) Everything A small codebase: no handoff, so nothing is lost between agents Discovery output fills the main context window

Exam rule: if a scenario says the second agent made mistakes "because it did not have the first agent's full findings", the answer is to fix the handoff: define what the first agent returns, store the full result where the second can read it, pass it unchanged, and verify at the end. "Use a bigger model", "tell it to be careful" or "run them in parallel" do not fix missing information. (General coordinator rules: Domain 1, coordinator handoffs.)

What "structured output" means in this domain

In Claude Code What it is Guarantee
Output section in a subagent's .md body Instructions for the report the subagent returns None: guidance. Check it with a file, a test or a hook
claude -p --json-schema '<schema>' "…" Validated JSON after the agent finishes (print mode only) Enforced by Claude Code
claude -p --output-format json The normal result wrapped in a JSON envelope (result text, cost, session id) Only the envelope is JSON; the result text is free text

The full map of all runtimes: Structured output: which runtime is the question about?

Iterative refinement: getting Claude from "close" to "right"

This is about how you talk to Claude Code during a task. No files are involved. Each technique fixes a different problem, so the exam gives you the problem and asks for the technique.

The problem in the question The technique What you actually do
Your description in words is read differently each time Concrete input/output examples Show 2–3 real inputs with the exact output you expect. Examples beat more adjectives.
The code "mostly works" and you need a clear target Tests first, then share the failures Write tests for the expected behavior, edge cases and limits. Run them. Paste the failing output back to Claude. Repeat.
The domain is new to you, and you don't know what you don't know The interview pattern Ask Claude to ask you questions before it writes code (for example cache invalidation, failure modes).
Several problems at once One message or several Problems that affect each other → describe them all in one detailed message. Independent problems → fix them one at a time.
Example: concrete input/output examples instead of more words

You first wrote: "Normalize the phone numbers." Claude kept the spaces in one run and removed the country code in another. Instead, say:

TEXT
Normalize phone numbers to E.164. Examples:

  "(415) 555-0123"      → "+14155550123"
  "415.555.0123 ext 9"  → "+14155550123"   (drop the extension)
  "+44 20 7946 0958"    → "+442079460958"  (keep a foreign country code)

Three examples settle what the words could not: the format, the extension rule, and the foreign-number rule.

Example: tests first, then iterate on the failures
TEXT
Turn 1:  "Write tests for parse_amount(): '12.50' → 1250 cents, '' → error,
          '1,234.5' → 123450, None → error. Don't write the function yet."
Turn 2:  "Now implement parse_amount() so these tests pass."
Turn 3:  (you run the tests) "2 failing:
          test_none_input: TypeError instead of ValueError
          test_thousands_separator: got 1234 (expected 123450)
          Fix only these."

The test output is an objective target. It is better than "it still doesn't handle some cases".

Example: the interview pattern
TEXT
"I need a caching layer for the product API. Before you write any code,
ask me the questions you need answered: what invalidates the cache,
what happens when the cache is down, how stale data may be, and anything
else I might not have thought of."

Claude asks first. You answer. Then it builds. Use this when the area is new to you, so the design surprises come before the code, not after.

One message or several? Problems interact when fixing one changes the other. For example, a changed date format breaks the sorting, and the sort decides which rows you deduplicate. Send those together with the full picture, or each fix will undo the last one. Problems that don't touch each other (a typo in a log line, a missing null check in another module) are faster and safer one by one.

Exam cue: the stem names the symptom. "Interpreted inconsistently" → examples. "Edge cases keep failing" → tests and the failure output. "Unfamiliar domain, unknown risks" → interview. "Fixes keep breaking each other" → one combined message.

From solo development to CI and team enforcement

CI means continuous integration: an automated runner checks each proposed code change. A solo developer can use it too. You do not need to remember to run tests in your terminal. A workflow in the repository runs them each time someone opens or updates a pull request. CI is a program; it is not necessarily an agent. CI glossary.

The whole enforcement picture

This diagram keeps two things apart. The first is which settings win inside Claude Code (on your machine). The second is separate checks on the repository server (for example GitHub). The arrows show influence or the next step. They do not show the exact internal order of every permission check.

flowchart TD
    M["1. Admin-managed Claude Code policy"] --> R["Claude Code runtime permissions and hooks"]
    C["2. CLI, local, project, user settings"] --> R
    I["CLAUDE.md and user prompt: model guidance"] --> A["Model proposes a tool call"]
    A --> R
    R -->|Denied| X["Action blocked"]
    R -->|Permitted| W["Working-tree change"]
    H["Other editor or manual terminal"] --> W
    W --> P["Pull request on repository server"]
    P --> T["CI runner: formatting, lint, tests"]
    T --> G["Required checks and reviews"]
    G -->|Fail or missing| B["Merge blocked"]
    G -->|Pass with authorized actor| F["Protected branch updated"]
    K["Runner credentials: repository read only"] --> T

Two separate questions: First, may this agent run a command? Claude Code answers that. Second, may this change enter the protected branch? The repository server answers that. Passing the first check does not pass the second. A person who edits in another editor skips Claude Code hooks completely. But their proposed changes still meet the repository checks.

Where the configuration actually goes

Job Location / owner Example
Explain conventions (guidance) Team’s CLAUDE.md “Use the repository formatter and run tests.”
Share local automation (Claude Code) Team’s .claude/settings.json plus check scripts Register a formatter to run after edits.
Apply required agent policy (Claude Code, enforced) Settings managed by an admin, plus hook scripts nobody else can edit Block forbidden operations in managed Claude Code sessions.
Run CI checks (repository server) .github/workflows/checks.yml in the repository Run format checks, lint, and tests on pull requests.
Give the runner the smallest access Workflow token permissions and CI credential configuration Read repository contents; give no token or SSH key that can write.
Require success before merging (repository server) Repository settings: rulesets or branch protection Require the named CI check and reviews; limit who can bypass.

Settings hierarchy: what can a local override change?

Layer, strongest first Team interpretation
Managed policy Requirements an admin controls; lower layers cannot simply replace them.
Command-line options Choices for one session, inside the managed limits.
Project-local settings Your personal preferences for one project, inside the limits.
Shared project settings Team defaults and rules, kept in git.
User settings Your personal defaults for all projects.

The rule is not “the last file wins” for every key. Claude Code combines the permission lists from all layers, and a matching deny beats an allow. So an allow in your local file does not cancel a deny that another layer loaded. But a developer who can edit the project files can also change a normal project policy. So put required Claude Code controls in managed policy (set by an admin). Put rules for the whole repository in server controls (on GitHub). Permission evaluation↗ · Managed deployment↗.

Concrete CI example · tests run without Claude and without push permission

🔒 GitHub runs this file. The triggers (on:), the job's permissions: and the secrets are applied by GitHub, not by the model. The prompt and the @claude comment are guidance that Claude reads.

With Claude Code's files: Claude Code in the job also reads the repo's CLAUDE.md (guidance) and any settings you pass with the settings input or claude_args (enforced).

Illustrative GitHub Actions file: .github/workflows/checks.yml. It assumes a Node project with a committed lockfile and format:check, lint, and test scripts in package.json. It is an example for teaching; this lesson does not install it.

YAML
name: Repository checks
on: [pull_request]

permissions:
  contents: read

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: npm
      - run: npm ci
      - run: npm run format:check
      - run: npm run lint
      - run: npm test

The CI runner executes these run: lines as ordinary commands. They do not pass through Claude Code’s Bash tool or through .claude/settings.json. The line permissions: contents: read limits what this job’s GitHub token may do. It does not control Claude’s tools, and it does not control every other credential. If the job must not push, do not also give it a personal token with write access, an SSH key, or another such credential. The checkout setting stops the checkout step from saving its login for later Git commands. GitHub token permissions↗.

A failing test makes the job fail. That alone does not stop a merge. An administrator must also mark this check as required for the target branch, and must set who may bypass the rule. Changes to the workflow and check scripts also need review controls. Adding this YAML file by itself does not create a merge gate. Protected branches and required checks↗.

If CI also runs Claude Code · there are two permission systems

Sometimes a workflow also starts Claude Code, for example to explain a failure. Then you must also set Claude Code’s tool permissions. Here is an illustrative piece of a Claude Code settings file:

JSON
{
  "permissions": {
    "allow": ["Bash(npm test)"],
    "deny": ["Bash(git push *)"]
  }
}

This pre-approves a matching test command. It also denies matching push commands, but only through the Bash tool. It is not a complete guarantee that nothing can push. Other tools, wrapper scripts, and direct API calls need their own controls. The CI job’s credentials must also have no write access to the repository. If this rule is required agent policy, deploy it through managed configuration. Do not assume that a file in the checkout is safe from changes. A normal CI job that only runs tests needs no Claude Code configuration at all.

What hooks guarantee, and what they do not

Mechanism What actually happens Limit
Formatter hook after an edit (Claude Code) Runs the configured formatting code on the edits it covers. Other ways of editing, errors, or missing configuration can leave files unformatted.
PreToolUse guard (Claude Code) Can reject a covered call before it runs. Needs correct matching and checking; protects only this Claude Code path.
PostToolUse check (Claude Code) Checks the result after the action. Cannot stop an action that already happened.
Required CI format check (repository server) Rejects code that fails the format check before merge. Must be marked required, and protected so nobody can skip or change it without permission.

Hooks run real code, but a hook enforces only what its code checks. A lower-level disableAllHooks setting cannot turn off managed hooks. Administrators can also limit where hooks may come from with allowManagedHooksOnly. And the hook scripts themselves must be protected so that nobody can edit them without permission. Hook configuration and managed hooks↗.

For a team: Aim for this guarantee: changes that break the rules cannot enter the protected branch. Do not claim that no developer can ever create a rule-breaking file on their own machine. Administrators, people with bypass rights, and outside credentials are still part of what you must trust.

Recall: Instructions (CLAUDE.md) explain. Hooks check the events they cover. Managed policy limits the agent. CI tests the change. Repository rules block the merge. Credentials limit what can be done on remote systems. Review tool availability versus permission.

Claude Code in CI/CD (scenario 5 — CI/CD automation — territory)

GitHub Actions: set it up as an admin, then what happens on each event

The Claude Code GitHub Action (anthropics/claude-code-action) runs Claude Code inside a normal GitHub Actions job. Nothing runs by magic: a workflow file decides which GitHub events start Claude. You need admin access to the repository for setup.

Quick setup (one command)

  1. Install the GitHub CLI and run gh auth login.
  2. Open claude in the repo and run /install-github-app. It works with github.com repos only.
  3. Claude Code installs the Claude GitHub App and saves a repo secret: ANTHROPIC_API_KEY (API key) or CLAUDE_CODE_OAUTH_TOKEN (Claude subscription token).
  4. It pushes a branch with the workflow files you picked and opens a pull request. Merge it, and @claude works.

Manual setup (you do each step)

  1. Install the Claude GitHub App (github.com/apps/claude) on the repo. The action needs three of its permissions: Contents, Issues and Pull requests, all read and write. Installing the app grants its full permission set; GitHub doesn't let you accept a subset.
  2. Add a repo secret: ANTHROPIC_API_KEY (from the Claude Console) or CLAUDE_CODE_OAUTH_TOKEN (run claude setup-token locally). In the workflow, pass it to anthropic_api_key or claude_code_oauth_token.
  3. Copy examples/claude.yml from the action's repo into .github/workflows/claude.yml.
  4. Test it: write @claude in an issue or PR comment.

Rolling out to a whole organization

📄 .github/workflows/claude.yml · interactive @claude workflow (from the docs)

🔒 GitHub runs this file. The triggers (on:), the job's permissions: and the secrets are applied by GitHub, not by the model. The prompt and the @claude comment are guidance that Claude reads.

With Claude Code's files: Claude Code in the job also reads the repo's CLAUDE.md (guidance) and any settings you pass with the settings input or claude_args (enforced).

No prompt input, so this is interactive mode: Claude waits for @claude and answers in a comment.

YAML
name: Claude Code
on:
  issue_comment:
    types: [created]
  pull_request_review_comment:
    types: [created]
jobs:
  claude:
    if: contains(github.event.comment.body, '@claude')
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
      issues: write
      id-token: write
      actions: read
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}

actions/checkout gives Claude a copy of the repo. actions: read lets Claude read CI results on PRs. The if: line stops runners from starting on comments without @claude.

📄 .github/workflows/claude-code-review.yml · automatic PR review workflow (from the docs)

🔒 GitHub runs this file. The triggers (on:), the job's permissions: and the secrets are applied by GitHub, not by the model. The prompt and the @claude comment are guidance that Claude reads.

With Claude Code's files: Claude Code in the job also reads the repo's CLAUDE.md (guidance) and any settings you pass with the settings input or claude_args (enforced).

It has a prompt input, so this is automation mode: it runs on every matching PR event, with no mention needed. Quick setup names this file claude-code-review.yml.

YAML
name: Code Review
on:
  pull_request:
    types: [opened, synchronize, ready_for_review, reopened]
jobs:
  review:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: read
      id-token: write
    steps:
      - uses: actions/checkout@v6
        with:
          fetch-depth: 1
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
          plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
          plugins: "code-review@claude-code-plugins"
          prompt: "/code-review:code-review --comment ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
          claude_args: '--allowedTools "mcp__github_inline_comment__create_inline_comment"'

--comment makes Claude post the review on the PR. Without it, the findings are only in the run log. Keep the claude_args line: the action starts the inline-comment MCP server only when --allowedTools names it.

What happens when…

Event Which workflow fires What Claude does Where the output appears What it reads
Someone comments @claude … on a PR or an issue claude.yml (interactive mode, triggered by issue_comment / pull_request_review_comment) Does what the comment asks: answers, implements, fixes, pushes commits A comment on the same issue or PR, which Claude updates as it works The repo checkout, CLAUDE.md at the repo root, the comment, CI results (actions: read)
A PR is opened, updated, reopened or marked ready Only a workflow with on: pull_request, like claude-code-review.yml. With only claude.yml, nothing happens Runs the code-review skill on that PR. Skips drafts, closed PRs, trivial/automated PRs, and PRs it already commented on Inline comments on the PR, or one summary comment when it finds nothing (because of --comment) The repo checkout and the PR. Fork PRs on public repos don't get secrets, so the review runs only for same-repo branches
A PR is merged, or someone pushes to main Nothing, unless you wrote a workflow that triggers on push. Neither docs example does Nothing Nothing Nothing
A scheduled run (on: schedule with cron) Your scheduled workflow, in automation mode (prompt set) Runs the prompt. It has no shell or GitHub API access until you grant tools with --allowedTools or a permissions.allow rule in settings The workflow run log, by default Whatever its tools reach. The docs' daily-report example skips checkout and reads commits and issues through GitHub MCP tools. GitHub runs schedules only from the default branch
A user without write access, or a bot, tags @claude The workflow starts, but the action's checks reject the actor and the run fails Nothing A failed run —

On who may trigger: on issue and PR events the user needs write access, unless you list them in allowed_non_write_users (and pass your own github_token). Bots are always rejected unless listed in allowed_bots, so bots can't loop Claude. This bot check also applies to scheduled runs. A schedule skips only the write-access check.

Note. Claude Code in the action reads CLAUDE.md from the checked-out repo, so put your standards there and keep it short (it is read on every run). Settings go in through the settings input (JSON or a file path) or CLI flags in claude_args; secrets live only in GitHub Secrets, never in the repo.

The @claude tag — how a TEAM works with Claude on GitHub

A solo developer works with Claude in the terminal. A team works with it as a teammate you @-mention on GitHub. The parts are the Claude Code GitHub App plus a workflow file committed to the repo. An admin sets it up once; then everyone with write access can use it. Who does what (the actor test, for an organization):

Actor Does Where
Org admin (once) installs the GitHub App (/install-github-app from any Claude Code session, or by hand), puts ANTHROPIC_API_KEY in the org or repo secrets, and commits the workflow file GitHub org settings + .github/workflows/claude.yml
Any teammate with write access writes the tag: @claude … in an issue, issue comment, PR comment, or PR review comment. This is the trigger the user chooses — the team's version of /name issues & PRs
The Action (a runner) starts on the comment webhook, checks the author's permission, checks out the repo, and runs headless Claude Code — the same harness, with no terminal GitHub Actions runner
The model reads the thread and the repo, then WRITES: answers, commits to the PR branch, or a new branch + PR. It reports progress by editing one checklist comment through the Claude API
Humans again review the PR like any teammate's PR. Branch protection and required reviews are the GATE (enforcement); the tag never merges anything PR review
sequenceDiagram
  participant T as Teammate
  participant GH as GitHub (entry point)
  participant A as Action runner — headless Claude Code
  participant API as Claude API
  T->>GH: "@claude fix the null check in checkout.ts" (PR comment)
  GH->>A: webhook fires workflow (author has write access ✓)
  A->>A: checkout repo — CLAUDE.md + .claude/settings.json load (NO user layer in CI)
  A->>API: the loop: read thread + code → tool_use → edits
  A->>GH: pushes commit to the PR branch + updates its checklist comment
  T->>GH: reviews the diff, approves, merges — the human gate

🧭 The YAML holds no behavior. The workflow file controls exactly three things: WHEN it runs (events + the @claude trigger), WHAT it may touch (permissions:), and WHICH secret it uses. Every convention, rule, and skill that the run follows comes from the layers committed in the repo, loaded at checkout. So “will @claude follow X?” always means “is X committed to the repo?”, never “is X in the YAML?”. (And in exam language, “her personal conventions” means ~/.claude/CLAUDE.md — the one layer that no runner ever has.)

The workflow file, with notes (format correct as of mid-2026 — check current docs for exact versions and inputs):

YAML
# .github/workflows/claude.yml — committed once; every @claude mention flows through here
name: Claude
on:
  issue_comment:            { types: [created] }   # "@claude" in issue/PR comments
  pull_request_review_comment: { types: [created] }   # …and on PR review lines
  issues:                   { types: [opened, assigned] }
permissions:
  contents: write           # push branches/commits
  pull-requests: write      # open/update PRs
  issues: write             # reply on issues
  id-token: write
jobs:
  claude:
    if: contains(github.event.comment.body, '@claude')   # the tag IS the trigger phrase
    runs-on: ubuntu-latest
    steps:
      - uses: anthropics/claude-code-action@v1
        with:
          anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}   # org/repo SECRET — never in the file

“Where do I type claude -p?” — you don't. The three review lanes, demystified

claude -p "review this diff" confuses everyone who works in an interactive session. In YOUR daily work, commits happen inside the session (Claude proposes, you approve), and you never go to a terminal to review. Here is the answer: headless commands are written in YAML, not typed by a person. They live in a committed workflow file, and a RUNNER runs them when events happen. There are three review lanes, and each one is started by someone different:

Lane Initiated by Runs where When it reviews Your solo equivalent
In-session review you, in the conversation (/code-review, “review this before committing”) your interactive session before the commit exists anywhere this is already your daily work
Tag-triggered (@claude review) yes — really a person on the GitHub PR page typing a comment Actions runner (headless, started by the comment webhook) after the push, when asked, during the discussion on the PR you would only see this when reviewing a teammate's PR
Automatic CI review nobody — the workflow's on: pull_request event Actions runner (headless — the YAML “types” claude -p/the action for you) every PR, always a committed file, so nobody has to remember

And here is the team picture that explains why files are committed: teammates commit exactly like you do. Each works in their own session or editor, pushes branches, and opens PRs. The lanes do not replace human review; they add layers under it. A mature setup uses all three. Authors review in the session before pushing (lane 1). The workflow reviews every PR automatically (lane 3). People @-mention Claude on specific lines when they want a conversation (lane 2). And branch protection still requires a HUMAN approval to merge.

🧭 Your ai-partner-review habit (Gemini/Codex/Grok on the same diff) is lane 1 with one extra idea that the exam rewards: a judge from a different model family. It is the same principle as Domain 1's “never grade your own homework”, used for code review. A team would build the same idea as a second workflow in lane 3.

Three points for an architect; each one can come up in an exam or interview:

What actually happens after @claude, step by step, and who reads the JSON

Each step names who acts. The action here is anthropics/claude-code-action (a program that Anthropic publishes; it runs Claude Code through the Agent SDK).

  1. A person writes a comment on a PR: @claude fix the null check in checkout.ts. GitHub (the website) saves it and creates an issue_comment event.
  2. GitHub Actions (GitHub's job system) finds each workflow file whose on: matches that event. The if: line checks that the comment contains @claude. If it does not, no job starts and nothing is charged.
  3. GitHub starts a runner: an empty virtual machine (runs-on: ubuntu-latest). It puts the secret (ANTHROPIC_API_KEY) into the job's environment.
  4. The actions/checkout step copies the repository onto the runner (a git clone at the PR's commit). Now CLAUDE.md, .claude/settings.json and the skills are files on that disk.
  5. The action checks that the person who commented has write access and is not a bot. Then it posts a "working…" comment (the tracking comment). It collects the context (the comment, the PR title and description, the diff, the earlier comments) and builds a prompt from it.
  6. Claude Code, headless, runs the normal agent loop in the checkout folder. It sends a request to the Claude API. Claude answers with tool_use blocks: read a file, edit it, run the tests with Bash. Claude Code on the runner runs each tool on the runner's copy and sends the tool_result back. This repeats until Claude gives a final answer (or --max-turns stops it).
  7. The action sends the results to GitHub through GitHub's API, with the Claude GitHub App's token. It commits the changes and pushes them to a branch: the PR's branch, or a new branch for an issue. It updates the tracking comment with a summary. A review workflow can also post inline comments through a tool that the action provides (mcp__github_inline_comment__create_inline_comment).
  8. GitHub deletes the runner.
  9. People review the new commits like a teammate's work. Branch protection (rules that an admin set) decides whether the merge button works. The action never merges.

🧭 Anthropic also offers Code Review, a managed product: it reviews every PR automatically with no workflow file. It is a different product from this action, and it uses the same Claude GitHub App.

Where is the JSON, and who makes the decision?

With the action, you never handle JSON. The action reads Claude Code's output events itself and turns them into commits and comments. You can see the events in the workflow run log.

With your own claude -p step, your script receives JSON. --output-format json prints one JSON object when the run ends. The answer text is in result (illustrative, fields shortened):

JSON
{
  "type": "result",
  "subtype": "success",
  "is_error": false,
  "result": "Found 2 issues: an unchecked null in checkout.ts line 41, and a missing test.",
  "session_id": "3f1c…",
  "num_turns": 6,
  "total_cost_usd": 0.31
}

Add --json-schema and the answer also arrives as an object that matches your schema, in the field structured_output. Now your script, not Claude, turns that object into a decision:

The schema, saved in the repo as .github/review-schema.json (illustrative):

JSON
{
  "type": "object",
  "properties": {
    "findings": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "file": {"type": "string"},
          "severity": {"type": "string", "enum": ["critical", "minor"]}
        },
        "required": ["file", "severity"]
      }
    }
  },
  "required": ["findings"]
}
BASH
# CI step (illustrative). Claude reports; this script decides.
out=$(gh pr diff "$PR" | claude -p "Review this diff for security bugs." \
  --output-format json \
  --json-schema "$(cat .github/review-schema.json)")

critical=$(echo "$out" | jq '[.structured_output.findings[] | select(.severity == "critical")] | length')
if [ "$critical" -gt 0 ]; then
  echo "Blocking: $critical critical findings"
  exit 1          # non-zero exit code = this CI job fails
fi
Actor What it does with the result
Claude writes the findings (text, or JSON that matches the schema). It decides nothing about the merge
Your script (jq + if) reads the JSON and turns it into an exit code: 0 = pass, non-zero = fail
GitHub Actions shows a failed job as a red ✗ check on the PR
Branch protection (set by an admin) if that check is required, the merge button is blocked
A person reads the findings, fixes or disagrees, approves, merges

So "Claude approved it" is never the real mechanism. The decision is made by code you wrote and rules an admin set. Claude only produces the input to them. That is also why the exam separates an advisory Claude review (comments) from a blocking gate: a blocking gate should rest on checks that give the same result every time (tests, lint), or on a clear, written threshold in your script.

Claude Code GitHub Actions↗ · Run Claude Code programmatically (-p, --output-format, --json-schema)↗.

Recall: the nine steps, and who turns the JSON into a pass or a fail

Comment → event → workflow matches → fresh runner → checkout → the action checks permission and builds the prompt → headless Claude Code loop (Claude asks for tools, Claude Code runs them on the runner) → push to a branch + comments through GitHub's API → runner deleted → people and branch protection decide the merge. In your own claude -p step, your script reads result or structured_output and sets the exit code; GitHub shows the check; branch protection blocks or allows the merge.

Start fast and the same on every runner: --bare and the system prompt flags

Normally, Claude Code discovers its configuration by itself. At start-up the harness (the Claude Code program on the runner) looks for CLAUDE.md files and their @imports, hooks, skills, MCP servers and plugins. That is useful on your laptop. In CI it has two costs: start-up takes longer, and the result depends on what files happen to sit on that machine.

--bare turns that discovery off. The Claude Code docs say it reduces start-up time "by skipping auto-discovery of hooks, skills, custom commands, subagents, installed plugins, MCP servers, auto memory, and CLAUDE.md". The docs give the reason: it "is useful for CI and scripts where you need the same result on every machine".

Under --bare What happens
Skipped CLAUDE.md (and so its @imports), auto memory, hooks, skills, custom commands, subagents, installed plugins, MCP servers from .mcp.json
Login Claude Code never reads OAuth credentials or the system keychain. Give it ANTHROPIC_API_KEY (a CI secret) or an apiKeyHelper in --settings
At run time Only MCP servers given on the command line connect. No system reminders, no background tasks
Bring back what you need --append-system-prompt-file (instructions) · --settings <file> · --mcp-config <file> · --agents <file> · --plugin-dir <path>
Not documented Whether settings.json files are still read, and whether @imports inside an appended file are resolved. Do not rely on either

The docs call --bare "the recommended mode for scripted and SDK calls" and say it "will become the default for -p in a future release". (Before v2.1.286 these limits held only partly.)

Append or replace the system prompt? The system prompt is the standing instructions that Claude Code sends to the model with every request. Two flag families change it. Each one also has a -file form that reads the text from a file.

Flag What it does to the default prompt Use it when
--append-system-prompt / --append-system-prompt-file Appends your text. Claude Code's own tool guidance and safety instructions stay Claude should still be a coding assistant that also follows your extra rules (a review, a fix, a refactor)
--system-prompt / --system-prompt-file Replaces the entire default prompt The surface, identity or permission model is different. You now own everything the task still needs, including tool guidance and safety instructions

Worked case. The ACME review job is slow to start. The repo's CLAUDE.md has many @imports, but the review needs only one file of review standards. Which command?

BASH
# Illustrative CI step. Secret ANTHROPIC_API_KEY is in the job environment.
gh pr diff "$PR" | claude -p --bare \
  --append-system-prompt-file .github/review-standards.md \
  --output-format json \
  --json-schema "$(cat .github/review-schema.json)" \
  "Review this diff against the appended standards."
Option What it gives the job Verdict
claude -p (no --bare) CLAUDE.md and all its imports, plus hooks, skills and MCP servers Slow start, the problem we started with
claude -p --bare alone A fast start, but CLAUDE.md is skipped, so the standards are gone Tempting, because it fixes the speed. It silently drops the one file the review needs
claude -p --bare --system-prompt-file <file> The standards file, but it replaces the default prompt, so tool guidance and safety instructions are gone too Tempting, because it loads the file. But a review job still uses tools; replacement is for a different kind of surface
claude -p --bare --append-system-prompt-file <file> A fast start, plus the standards on top of the normal coding-assistant prompt Choose this

Two more limits for unattended runs (both print mode only): --max-turns <n> stops the agent loop after n turns and exits with an error. --max-budget-usd <amount> stops the run when the estimated API spend reaches that amount (an estimate on the client, so it can differ from the bill). For permissions, --permission-mode dontAsk is the mode the docs name for CI pipelines, and plan mode keeps its blocks in -p runs too.

Start faster with bare mode↗ · CLI reference: system prompt flags↗.

Recall: the job is slow because of CLAUDE.md imports and needs one standards file. Which flags, and why not the two look-alikes?

claude -p --bare --append-system-prompt-file <file>. --bare alone is fast but drops CLAUDE.md, so the standards disappear. --system-prompt-file loads the file but replaces the whole default prompt, including tool guidance and safety instructions. Bare mode also needs ANTHROPIC_API_KEY, because it never reads OAuth or the keychain.

Hooks — deterministic control (the harness inside Claude Code)

Hook registration versus script location

Open the hook glossary for the same complete examples.

Where is a hook defined? Start with .claude/settings.json

For a hook the whole project shares, put the registration under hooks in .claude/settings.json. Put the program it runs in a separate file, for example .claude/hooks/after_edit.py. CLAUDE.md is instruction text (guidance). Writing “run this after every edit” there does not register anything to run on an event. A script file alone does not register itself either.

File or source Purpose
.claude/settings.json Hook registration for the project; shared in git.
.claude/settings.local.json Your personal hook registration for this project; normally kept out of git.
~/.claude/settings.json Your personal hook registration for all projects.
Managed settings Hook registration controlled by the organization's admins.
Plugin hooks/hooks.json Hooks that come with an enabled plugin.
Skill or agent YAML frontmatter Hooks that apply only to that skill or agent while it is active. This is structured metadata, not text in CLAUDE.md.
.claude/hooks/after_edit.py An example script location; you choose it. The command in the registration must point to it.

1 · Complete settings file: register the event

Example .claude/settings.json. If the file already has settings, add this hooks entry to them; do not overwrite the file. This example watches Write/Edit tool calls that succeeded.

JSON
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/after_edit.py\""
          }
        ]
      }
    ]
  }
}

hooks is a top-level settings key. PostToolUse chooses the event. matcher chooses the tool names. The inner hooks list holds the handlers. type: command tells Claude Code to start a command. CLAUDE_PROJECT_DIR gives the project folder. This is Claude Code configuration. It is not JSON that you send to messages.create.

2 · Complete script: read the hook event

Example .claude/hooks/after_edit.py. This safe teaching handler reads the JSON it receives on standard input and prints a diagnostic message. It does not format or change the file. Python 3 must be installed where Claude Code runs.

PYTHON
import json
import sys

event = json.load(sys.stdin)
tool = event.get("tool_name")
if tool in {"Write", "Edit"}:
    print("after_edit hook ran after " + tool, file=sys.stderr)
sys.exit(0)

Claude Code sends the event data when the hook runs. The model does not call this Python file itself. Exit 0 means the handler finished successfully. The diagnostic message is not guaranteed to reach the user or the model. Use /hooks to see the registration, and debug output to see whether it ran.

3 · What happens when Claude edits a file?

TEXT
Claude requests the Edit tool
  → Claude Code applies tool checks and runs Edit
  → Edit succeeds
  → Claude Code emits PostToolUse
  → matcher Write|Edit matches Edit
  → Claude Code launches after_edit.py and supplies event JSON on stdin
  → script reads the event and exits
  → Claude Code continues the agent loop

Claude Code starts this automatically; you do not type the script name after each edit. This matcher does not cover changes made through Bash or by another application. A PostToolUse handler runs after the edit, so it cannot stop that edit. For a check that must block a proposed action, use the PreToolUse event and follow its input/output rules.

See the official hook location guide↗.

Command hooks are Claude Code settings that run programs at events in the session (other hook types also exist). A hook can do one of three things: observe, block (exit code 2, on supported events like PreToolUse), or do extra work (for example, format a file right after a write). The key difference from instructions: code makes these happen (enforcement); they are not polite requests in text (guidance). Timing matters most here. A PreToolUse hook runs before the action and can stop it completely. A PostToolUse hook runs after the action already happened. It can format or test the file that was just written, but it cannot undo the write. Pre or Post? The side-by-side table, with a blocking example.

Event Fires Typical architect use
PreToolUse before a tool runs (matcher on the tool name) block writes to protected paths; require approvals; check the command
PostToolUse after a tool succeeds format written files; run tests; log
UserPromptSubmit when the user sends a prompt add context; validate
Stop / SubagentStop when Claude (or a subagent) finishes enforce the "done" criteria; send a notice
PreCompact / SessionStart before compaction / at start save state; load context

Exit code 2 from a PreToolUse hook blocks the action and sends stderr back to Claude. That is the enforcement loop.

The full registration and handler above show where hook code belongs. For hooks that block, follow the documented PreToolUse input/output rules. Do not assume that a $FILE variable exists automatically. The hook receives the event data as JSON on standard input.

Check the mechanism: Instructions explain the behavior you want (guidance). Hooks run checks at supported events (enforcement in Claude Code). Permissions and sandbox controls limit actions (enforcement). Check what your hooks really did, and use CI for checks that must cover every change.

Tool availability versus permission: where the lists live

Your API mental model is correct: In a Messages API request, your app sends the tool definitions in tools. The extra allow and deny options belong to the runtime around the model, not to the API request. So first decide which runtime the question is about: Messages API, Claude Code, or Agent SDK. Only then read its “allowed list.” Quick glossary.

Environment Where you configure it What it means
Messages API, including the normal Anthropic client SDK Request tools array Definitions of operations the model may call. The handlers for your own tools live in your application.
Your custom API harness Your own code that registers tools, sends calls to them, and checks authorization You build any allowlist yourself. The Messages API has no general allowedTools field.
Claude Code CLI --tools Chooses the built-in tools. This flag does not filter MCP tools.
Claude Code CLI --allowedTools / --disallowedTools Adds approval / denial rules. An allow entry is not a complete list of the only tools that exist.
Claude Code settings permissions.allow, permissions.ask, permissions.deny Rules that Claude Code enforces, stored in settings JSON — not text in CLAUDE.md.
Agent SDK options.tools, allowedTools, disallowedTools, mcpServers in TypeScript Your application code configures the Claude Code runtime. Python uses allowed_tools, disallowed_tools, mcp_servers.
Claude Code subagent definition YAML frontmatter tools, disallowedTools Limits what one worker can do. This is different from pre-approval at the top level.

The normal API client SDK and the Agent SDK are different libraries. The first one sends requests to the model. The second one runs a full agent runtime (the Claude Code engine). API tool definitions↗ · CLI flags↗ · Subagent configuration↗.

Claude Code example · built-in tools and settings

Run this command in your terminal, not in a model prompt:

BASH
claude --tools "Read,Bash" --allowedTools "Bash(git status)"

Read and Bash are names of built-in tools. The permission entry pre-approves only the matching shell command; it does not pre-approve every Bash command. You configure connected MCP tools in a separate place. To remove a tool, use the matching availability control or a deny rule. Deleting its allow entry is not enough.

Here is an example project .claude/settings.json. It assumes you already configured an MCP server named docs:

JSON
{
  "permissions": {
    "allow": ["Bash(git status)", "mcp__docs__load_document"],
    "deny": ["mcp__docs__archive_document"]
  }
}

An MCP tool name contains the server and the operation: mcp__docs__load_document. Built-in tools and MCP tools use the same permission rules, but you register them in different ways. User settings live in ~/.claude/settings.json. Your personal overrides for one project can live in .claude/settings.local.json. Managed policy can limit both. Permission rules and precedence↗.

Agent SDK example · register a custom tool, then configure permission

These TypeScript options go into the query({ prompt, options }) call in your application code. documentServer below is an SDK MCP server that you already created, with handlers that you wrote. This is a configuration example, not a complete program.

TS
const options = {
  tools: ["Read"],
  mcpServers: { docs: documentServer },
  allowedTools: ["Read", "mcp__docs__load_document"],
  disallowedTools: ["mcp__docs__archive_document"]
};

for await (const message of query({
  prompt: "Read the supplied document and explain its findings.",
  options
})) {
  // Your application consumes the SDK messages here.
}
🏭 Production shape: create the server, then read results

In a real program you also write the part the example skips: the tool() handler with its Zod schema, and createSdkMcpServer(). The Agent SDK turns the Zod shape into the tool's JSON input schema. Your loop reads the final result message instead of ignoring the messages.

TS
import { query, tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
import { z } from "zod";
import { documents } from "./documents"; // your repository/service module

const loadDocument = tool(
  "load_document",
  "Load one document's text by its id. Read-only.",
  { documentId: z.string().describe("The document id, e.g. doc-17") },
  async ({ documentId }) => {
    const doc = await documents.findReadable(documentId); // your access check lives here
    if (!doc) {
      return { content: [{ type: "text", text: `No readable document ${documentId}` }], isError: true };
    }
    return { content: [{ type: "text", text: doc.text }] };
  },
  { annotations: { readOnlyHint: true } }
);

const documentServer = createSdkMcpServer({ name: "docs", version: "1.0.0", tools: [loadDocument] });

for await (const message of query({
  prompt: "Read document doc-17 and explain its findings.",
  options: {
    model: process.env.CLAUDE_MODEL ?? "claude-sonnet-5",
    tools: ["Read"],
    mcpServers: { docs: documentServer },
    allowedTools: ["Read", "mcp__docs__load_document"],
    disallowedTools: ["mcp__docs__archive_document"],
    maxTurns: 8,
  },
})) {
  if (message.type === "result") {
    if (message.subtype === "success") console.log(message.result);
    else console.error(`Run stopped: ${message.subtype}`); // e.g. error_max_turns
  }
}

What the model sees for this tool (generated, shortened): the name mcp__docs__load_document and this input schema:

JSON
{
  "type": "object",
  "properties": { "documentId": { "type": "string", "description": "The document id, e.g. doc-17" } },
  "required": ["documentId"]
}

Where it lives: src/tools/documents.ts (tool + server) and src/agent.ts (the query() call). Package: @anthropic-ai/claude-agent-sdk with zod as a peer dependency.

The steps are: write a custom handler with tool(), wrap it with createSdkMcpServer(), then connect it through mcpServers. Registering the tool makes it available to the model. Putting its full name in allowedTools pre-approves its calls. If you name a tool that does not exist in an allow rule, that does not create the tool. Custom tool registration↗.

A tool that is not in the list is not automatically forbidden. Its calls follow the current permission mode and the other rules. The canUseTool callback in your code handles approval decisions that no rule settled. It may not run at all for pre-approved calls. So a check that must run on every call belongs in a hook or in the backend service that does the work. Agent SDK permissions↗.

Many pipelines in one repo: the project file is the floor, not the whole policy

One .claude/settings.json file cannot describe two pipelines that need different tools. It does not need to. Permissions stack in layers. A deny in any layer wins. allow entries from all layers are merged. The flags on one run sit above every file. So the shared file holds the rules that every pipeline in the repo must follow. Each pipeline then sets its own pre-approved list (and its own extra denies) in its own job definition.

Layer Where the list lives Scope
Per run claude -p … --allowedTools … --disallowedTools … --permission-mode …, or --settings pipelines/extract.settings.json loaded on top of the repo file; --mcp-config … --strict-mcp-config limits which MCP servers even exist for the run this one run only
Per agent .claude/agents/<name>.md frontmatter tools: (limits the worker); a skill's allowed-tools (pre-approves only, never limits) that worker, wherever it is started
Per project .claude/settings.json permissions.allow / permissions.deny every session and job in this checkout
Per machine / org ~/.claude/settings.json, managed policy no lower layer can remove a deny
JSON
// .claude/settings.json — the floor every pipeline in the repo shares
{ "permissions": { "deny": ["mcp__finance__update_database", "mcp__finance__send_alert", "mcp__finance__delete_record"] } }
BASH
# pipelines/extract.sh — narrows to its three tools; unattended, nothing outside the list can be approved
claude -p "Extract the filings in inbox/" --output-format json \
  --allowedTools "mcp__finance__read_file,mcp__finance__write_output,mcp__finance__lookup_vendor"

# pipelines/reconcile.sh — same repo, a different list; the shared denies still apply
claude -p "Reconcile yesterday's ledger" --output-format json \
  --allowedTools "mcp__finance__read_file,mcp__finance__lookup_vendor,mcp__ledger__post_entry"

Pipelines written as code (Agent SDK) do the same thing for each call. The query() options accept allowedTools and disallowedTools, so each pipeline function carries its own list. The model decides nothing about this at run time. The job definition is the policy.

Read an exam’s “allowed tools” precisely

Clue in the scenario Interpretation
“Tool absent from tools” in a custom API loop Check what your harness exposes and which calls it runs.
“Tool absent from allowedTools” in Claude Code / Agent SDK It is not pre-approved. That alone does not prove the tool is unavailable.
“Subagent has only these tools” Check that worker's tool scope and the configuration it inherits.
“allowedTools inside CLAUDE.md” Markdown text is guidance. It is not a permission rule that anything enforces.
“Forbidden operation must never execute” Enforce a deny rule and backend authorization. Do not rely on a prompt, or on leaving the tool out of an allow rule.

Suppose an agent must never archive a document by itself. There are two different fixes. You can remove the archive capability: no archive tool is exposed, and the agent's backend identity does not have that right. Or you can remove only its pre-approval: the tool still exists, but the runtime now asks a person before it runs. A separate case: if the MCP tools are already configured and reachable and the model picks the wrong one, the allowlist is not the cause. Look at the tool descriptions and how the model selects tools.

Recall: Registration provides the tool. Descriptions guide which tool the model picks. Permissions decide whether a call runs. Backend authorization checks the requested action on the resource. Checked against official documentation on September 22, 2026. Check your installed runtime version for the exact options it supports.

Developer tools: arguments and shell flags

Read, Write, Edit, Bash, Grep, and Glob are names of tools inside the Claude Code runtime. They are not macOS commands. The model picks a tool and fills in structured arguments. Then Claude Code (the runtime) checks permission, runs the tool's code, and sends the result back to the model. A plain Messages API call from your own app does not come with these tools.

flowchart LR
    M["Model requests named tool with JSON input"] --> R["Claude Code runtime checks and dispatches"]
    R --> F["Built-in file/search operation"]
    R --> B["Bash tool launches shell program"]
    R --> C["MCP client calls connected server tool"]
    F --> O["Result returned to model"]
    B --> O
    C --> O

Which operation should I choose?

Tool What it does Example and arguments
Read Reads a file's content without changing it. Open Read example
Write Creates a file, or replaces all of its content. Open Write example
Edit Replaces matching text in a file that exists. Open Edit example
Bash Runs a shell command through Claude Code. Open Bash example
Grep Searches for patterns inside files. Open Grep example
Glob Finds file paths that match a filename pattern. Open Glob example
MCP server tools Operations that a connected server adds. Discovery and dispatch example

Tool arguments are not shell flags

Compare two ways to search:

JSON
{"name":"Grep","input":{"pattern":"processPayment","path":"/project/src","output_mode":"content"}}
JSON
{"name":"Bash","input":{"command":"rg -n -F 'processPayment' /project/src","timeout":10000}}

These are parts of tool calls, not complete API requests. In the first one, JSON properties set the options of the Grep tool. In the second one, Claude Code passes command to a shell. That shell starts ripgrep (rg). Ripgrep, not the tool, reads -n (show line numbers) and -F (match literal text). The glossary dialogs below show complete illustrative model responses, including the call ID and the stop reason. Claude Code provides these tool definitions itself. Do not assume the same names exist in a custom harness that you build on the API.

Shell example inside Bash Flags belong to Meaning
rg -n -F 'processPayment' src/ ripgrep Match literal text and include line numbers.
rg -l 'processPayment' src/ ripgrep Return filenames containing matches.
rg --files -g '*.js' src/ ripgrep List JavaScript file paths, rather than matching content.
git diff -- src/payment.js Git Inspect that file's changes; -- separates paths from options.

Do not move a flag from one program to another. Do not write made-up commands such as Read --file. The shell command grep (lowercase), ripgrep rg, and the Claude Code Grep tool do similar jobs, but each has its own interface. Also, a glob pattern is a general idea: a pattern for filenames. The name Glob with a capital G means the Claude Code tool.

Using Read

Inspect a function before editing it.

Read reads a file's content and does not change it. file_path names the file. offset is the first line to read, and limit is the maximum number of lines. When PDF reading is supported, pages chooses a page range such as 1-5.

JSON
{
  "file_path": "/project/src/payment.js",
  "offset": 1,
  "limit": 40
}

This is only the Read input object. Expected result: the file content with line numbers, a notice that only part was read, or an error. Read does not run the code, and it does not list a directory. Open the complete call envelope and explanation.

Using Write

Create a new report file; use Edit for a small change to an existing file.

Write creates a file or replaces all of its content. file_path is where the file goes. content is the full new text of the file, not a patch.

JSON
{
  "file_path": "/project/docs/payment-notes.md",
  "content": "# Payment notes\nReview pending.\n"
}

This is only the Write input object. Expected result: a confirmation or an error. Writing a file changes the disk, so read the existing content before you replace it. Open the complete call envelope and explanation.

Using Edit

Rename repeated text when all matches are intended.

Edit replaces matching text in a file that already exists. old_string is the exact text to replace. new_string is the new text. When replace_all is true, Edit replaces every text match in this file.

JSON
{
  "file_path": "/project/src/payment.js",
  "old_string": "userData",
  "new_string": "customerData",
  "replace_all": true
}

This is only the Edit input object. Expected result: a confirmation, or an error when the match rules are not met. Text replacement does not understand the programming language, so it is not a true symbol rename. Check comments, strings, and unrelated parts of the code afterwards. Open the complete call envelope and explanation.

Using Bash

Run a test runner, git diff, or a shell text search.

Bash runs a shell command through Claude Code. command holds the shell command. timeout is the maximum run time in milliseconds for this tool. The -n and -F flags inside command belong to rg. They are not fields of the Bash tool.

JSON
{
  "command": "rg -n -F 'processPayment' src/",
  "timeout": 10000
}

This is only the Bash input object. Expected result: the command output and exit status, or a handle to a background task, depending on the runtime. Bash is the Claude Code tool. rg, git and python are separate programs, and they must be installed where the shell runs. Open the complete call envelope and explanation.

Using Grep

Find matching source lines, then inspect imports and aliases.

Grep searches for a pattern inside file contents. pattern is what to search for. path limits where to search. glob filters by filename. output_mode chooses content, files_with_matches, or count. head_limit caps how many results are shown.

JSON
{
  "pattern": "processPayment",
  "path": "/project/src",
  "glob": "**/*.js",
  "output_mode": "content",
  "head_limit": 30
}

This is only the Grep input object. Expected result: matching lines, matching paths, or counts, depending on output_mode. Grep is not Glob. It does not follow renamed imports, and it does not know which names refer to the same symbol. Open the complete call envelope and explanation.

Using Glob

Locate JavaScript test files before reading them.

Glob finds file paths that match a filename pattern. pattern describes the paths. ** crosses any number of folders, and * matches inside one part of the path. path is the folder to start in.

JSON
{
  "pattern": "**/*.test.js",
  "path": "/project"
}

This is only the Glob input object. Expected result: the matching file paths; watch for a notice that the list was cut short. Glob does not search the text inside those files. Open the complete call envelope and explanation.

Using MCP tools

An MCP server is not one extra general tool called “MCP.” The server publishes operations such as analyze_dependencies. Each has its own name, description, and input schema. The host (here, Claude Code) discovers them and shows the right definitions to the model. When the model picks one, the host sends the call through its MCP client. The server can be a local process or a remote service. The real operation names and arguments come from that server, not from some standard set of operating-system commands.

Imagine a registered server named codeintel. The host might show an operation called mcp__codeintel__analyze_dependencies with the input {"module_path":"src/payment.js"}. This name and schema are illustrative. Open discovery, call and result examples. The built-in Bash tool can also run a language tool directly. A command that helps with coding is not automatically an MCP tool.

Exam terminology versus your installed runtime

The official reference (checked 21 September 2026) says that, under the conditions it describes, Grep and Glob are not present by default on macOS, Linux and WSL. Claude Code may use shell commands instead. Windows, or an explicit configuration, can be different. Read the tool definitions in your installed version to see the supported arguments and output limits. Do not assume that an older exam question describes the exact current list of tools. Current Claude Code reference↗.

Claude Code runs built-in operations, and its permissions still apply. Bash commands depend on which programs are installed and on the platform; Windows may not have the same shell programs as macOS. MCP operations depend on the connected server and on what it authorizes. “Built-in” does not mean “no limits” or “the same on every machine.”

Walkthrough: find test filenames with Glob → search the relevant content with Grep → look at the file with Read → change a small part with Edit, or create a new file with Write → run the tests through Bash. Use an MCP dependency analyzer when you need the code analysis that its server really implements. Explore alias-aware reference searching.

Grep, Glob, and callers with different names

Grep searches the text inside files. Glob finds files by their paths. A symbol-aware reference search follows what a name means in the code. Claude Code's tools are spelled Grep and Glob. Shell commands such as grep and rg also search text, but a shell runs them and they have their own flags. Shorthand such as Grep "processPayment" --all-files is not a real tool call. The tool takes structured arguments, not command-line flags. Which tools exist, and their interfaces, depend on the runtime and its version. Claude Code tools reference↗.

Need Suitable operation What the result establishes
Find JavaScript files under api/. Glob/path pattern such as api/**/*.js. Which matching paths exist.
Find text matching processPayment. Grep, or shell rg. Where that spelling or pattern appears in the searched files.
Find uses of the real function, even through renamed imports. References from the IDE or language server, plus reading imports and exports. How the code connects, inside the supported, indexed project.
Find consumers outside this checkout. Search the consumer repositories you can access, dependency records, or usage data. Evidence from those outside systems; a local search cannot see them.

Follow one function through four names

An alias is a second name for the same exported value. In these examples, a re-export does not create a new payment function. The as keyword only changes the name that other files use to import it.

Read the files and predict the search matches
JAVASCRIPT
// core/payment.js
export function processPayment(order) {
  return { accepted: true, orderId: order.id };
}
JAVASCRIPT
// api/gateway.js
export { processPayment as handlePayment } from "../core/payment.js";
JAVASCRIPT
// services/billing.js
export { processPayment as executePayment } from "../core/payment.js";
JAVASCRIPT
// clients/web.js
import { handlePayment } from "../api/gateway.js";
handlePayment({ id: "order-web" });
JAVASCRIPT
// clients/batch.js
import { executePayment } from "../services/billing.js";
executePayment({ id: "order-batch" });
JAVASCRIPT
// clients/mobile.js
import { handlePayment as pay } from "../api/gateway.js";
pay({ id: "order-mobile" });

A search for processPayment finds the declaration and the two re-export lines. It finds nothing in the three consumer files, because none of them contains that spelling. A consumer can call the same function without ever writing its original name. Searching more files does not help when the query looks for only one name.

A search for handlePayment finds the web import and call, and the mobile import. But the mobile call itself says pay(...). You must read the import to find this extra local alias, and then follow its uses inside that file. Searching all three known names is a good next step. It still does not prove that you found every caller.

flowchart LR
    F["core: processPayment"] --> G["gateway re-exports as handlePayment"]
    F --> B["billing re-exports as executePayment"]
    G --> W["web calls handlePayment"]
    G --> M["mobile imports as pay and calls pay"]
    B --> C["batch calls executePayment"]

The arrows show one exported function becoming available under different names. A plain text search does not follow these arrows. A dependency graph shows which modules import from which other modules. Symbol resolution goes further: it tracks which exported value each local name points to. A module graph alone may still not show each individual place where the function is called.

Concrete searches, with their limits

These are ripgrep commands for the shell. They are not JSON arguments for the Claude Code Grep tool. Run them from the root folder of the illustrative project:

BASH
rg -n -F 'processPayment' .
rg -n 'processPayment|handlePayment|executePayment' .
rg --files -g '*.js' api

The first command searches for one exact spelling. The second uses a regular expression with alternatives to search all three known names. The third lists matching file paths; it does not search their content. By default ripgrep skips ignored and hidden files. Before you treat the results as complete, check the search scope, any cut-off results, and any skipped folders.

A match is not always a caller. It can be the declaration, an import, a comment, or a different function with the same name. Read the matching code. Follow imports and re-exports. Use a language server or the IDE's Find References when you have one. Check with tests and type checks before you change code. Some cases need more investigation even with language-aware tools: dynamic property access, functions registered at run time, generated code, and consumers outside this repository.

Why A is intended and C is reasonable

When a processPayment search misses callers, two diagnoses are both true, but at different levels. Call them A (the direct cause) and C (the deeper limit of the tool).

A — the direct cause: callers use a different spelling. After the function is re-exported as handlePayment and executePayment, the consumer files never contain the string processPayment. This one fact explains why the search is incomplete. It also points directly to the fix: search every known name, then read the imports to find more local aliases.

C — the deeper limit: Grep matches text, not import relationships. A text search does not know that three names point to one exported value. This is a real property of the tool, and it is why A can happen at all. Keep this rule. It tells you when to use a language server or the IDE's Find References instead of a wider regex.

The two overlap. The difference is how specific each one is. A is the exact diagnosis for this symptom. C is the general rule that predicts it. One detail separates them. A re-export chain that keeps the same spelling does not hide any text. If a consumer still writes processPayment(...), that line matches, even though Grep does not understand the chain. The changed spelling is what removes caller lines from the results.

Two diagnoses are wrong. The first treats “every place the string appears” as “every use of the function”; but a string can be a comment, an import, or a different symbol with the same name. The second switches to finding paths; but Glob only finds files to look at. It cannot see inside them, so it cannot solve an alias problem.

Decision habit: First ask what you need: a filename, a place where text appears, or a reference to one specific symbol. For an alias, write the chain original → exported alias → local import alias → call. Then check which of those names your search really includes.

Built-in editing tools: one match or all matches

Suppose userData appears twelve times in one file, and every one must become customerData. A careful method works: find each one, include enough nearby text to make it unique, and replace that one. But that is twelve edit calls. Claude Code's Edit tool has a replace-all mode: one call, one file, every exact match. The real decision is what scope the operation should have. Twelve separate edits are possible, but that is not the question.

Built-in tools: which job belongs where?

Tool Job in this example
Glob Find a file by a filename pattern when you do not know its path.
Grep Search inside files; shows where the old text appears.
Read Look at the file and decide which matches you really want to change.
Edit Replace exact text in one named file.
Write Give the full content of a file; useful to create a file, or to rewrite a whole file on purpose.
Bash Run shell commands, such as the test command or a diff review.

These are tools of the Claude Code runtime. A plain messages.create API call from your own app does not get them. Which tools exist depends on the runtime, platform, version, and configuration. For example, current releases may search through Bash instead of showing Grep/Glob by default. This section assumes that the named tools are available. Official tool reference↗

Grep the tool versus grep the command

Grep (the tool name) and grep (a command inside a shell) can both search text, but their interfaces are different. sed is a shell program that changes text. It is not a Claude Code tool named Sed. A Bash call can run sed if that program is installed where the shell runs. Bash itself is a built-in tool. So “built-in tool” does not mean “never uses shell commands.”

What you see What the model requests Who executes the operation
Grep with structured arguments A tool made only for searching content Claude Code's code for that tool
Bash with grep ... A shell command as text The shell that the runtime chose
Bash with sed ... A shell command that changes text The shell and the installed program
Edit with replace_all: true A structured, exact-text replacement Claude Code's code for Edit
Same search intention, two different tool-call structures

These two complete illustrative tool-use blocks search for userData in a known file. They are requests from the model. They are not terminal output, and they are not full API responses. The first path works only if the Grep tool is available. The second assumes that the shell command and the path exist.

JSON
{
  "type": "tool_use",
  "id": "toolu_search_dedicated",
  "name": "Grep",
  "input": {
    "pattern": "userData",
    "path": "/workspace/app/profile.js",
    "output_mode": "content"
  }
}
JSON
{
  "type": "tool_use",
  "id": "toolu_search_shell",
  "name": "Bash",
  "input": {
    "command": "grep -n 'userData' /workspace/app/profile.js"
  }
}

The outer name decides which tool Claude Code runs. In the second example, grep is only text inside the command argument of the Bash tool. A search returns information; neither example changes the file. To change the file, the model must send another request, to an editing tool.

What changes across operating systems?

Software that runs on many platforms can offer structured file tools and run commands in a supported shell. A shell command must fit the real environment. Installed programs, paths, quoting, and options can differ. For example, do not assume every platform accepts the same sed syntax for editing a file in place. On Windows you may have Git Bash, WSL, or a supported PowerShell setup. These are different environments, not different spellings of the same thing.

Do not guess which tools exist from the operating system alone. The current documentation says that in some configurations Grep/Glob are not present by default on macOS, Linux, and WSL, and search goes through Bash instead. The model can choose only from the tool list that the runtime exposes. Older exam material may assume a different default set of tools. Current Claude Code tool availability↗

Beyond file editing: recognize the tool families

This is a map for learning. It does not promise that every session has every tool. For the current full list and when each tool is available, use the official reference.

Family Example tools Example task
Files Read, Edit, Write, NotebookEdit Look at code, fix a function, create a file, change a notebook cell.
Search and code navigation Glob, Grep, LSP Find filenames, find matching text, or see where a symbol is used. LSP needs a language server.
Shell execution Bash, PowerShell where enabled Run the project's tests or build commands in its configured environment.
Web information WebSearch, WebFetch Find a source, then download a known URL.
Delegation Agent Give a limited investigation to a subagent with its own context.
Reusable procedures Skill Load a configured procedure and follow it with the available tools.
Planning and clarification EnterPlanMode, ExitPlanMode, AskUserQuestion Plan the work, or ask about an unclear requirement.
Work tracking TaskCreate, TaskList, TaskUpdate Record work and progress. Tracking a task does not make the code change.

How to reason through another tool-choice question

First, name the operation: find, look, change, run, or delegate. Next, name its scope: one occurrence, every match in one file, or the whole repository. Then check which available tool, with which arguments, does exactly that. Last, check the result. For the same rename everywhere in one file, this gives Edit + all intended matches in one named file. Working across platforms is useful context. But the difference that matters is twelve targeted edits versus one replace-all operation.

Two Edit modes: one occurrence or every match

One occurrence: Without replace-all, the old text must match in exactly one place. If the short string appears many times, add enough nearby text to make it unique.

All occurrences: Set replace_all: true when every exact match in that file should get the same replacement. It is an argument inside the Edit call. It is not a global setting in settings.json, and it is not a top-level Messages API option.

Below are complete illustrative tool-use blocks. The model proposes the call. Claude Code checks permissions and runs it. The IDs and paths are examples. These blocks are not full Messages API responses.

One Edit request replaces all intended matches
JSON
{
  "type": "tool_use",
  "id": "toolu_rename_all",
  "name": "Edit",
  "input": {
    "file_path": "/workspace/app/profile.js",
    "old_string": "userData",
    "new_string": "customerData",
    "replace_all": true
  }
}

For a small illustrative file:

JAVASCRIPT
// Before
const userData = loadProfile();
renderProfile(userData);
saveProfile(userData);

// After
const customerData = loadProfile();
renderProfile(customerData);
saveProfile(customerData);

This small example has three matches; a real file might have twelve. The operation is the same. file_path selects one file. Replace-all does not search every file in the repository.

Use unique surrounding text when only one match should change

Suppose only the render call should use a different value, and loading and saving must stay the same. Then this targeted call says exactly that:

JSON
{
  "type": "tool_use",
  "id": "toolu_target_one",
  "name": "Edit",
  "input": {
    "file_path": "/workspace/app/profile.js",
    "old_string": "renderProfile(userData);",
    "new_string": "renderProfile(customerData);",
    "replace_all": false
  }
}

The nearby renderProfile(...) text points to one place. With twelve matches of userData, old_string: "userData" without replace-all is unclear, and the tool does not guess which one you meant. Adding unique nearby text is the right method when only some occurrences should change, or when different occurrences need different replacements. For the same rename everywhere, it creates twelve edit calls where one is enough.

Follow the runtime flow

sequenceDiagram
    participant M as Claude model
    participant H as Claude Code runtime
    participant F as File
    M->>H: Read file
    H->>F: Load current contents
    F-->>M: Contents through tool result
    M->>H: Edit(old_string, new_string, replace_all=true)
    H->>H: Check permission and matching conditions
    H->>F: Apply replacements in the specified file
    H-->>M: Tool result reports outcome
    M->>H: Inspect diff and run relevant checks

Reading the file first is good practice here. Whether the runtime forces a read before an edit depends on the model and runtime version. Do not memorize it as a rule for all versions. The skill the exam tests here is replace-all.

Decision rule — four ways to do the rename, and what each costs:

Text replacement is not semantic refactoring

Before you use replace-all, check strings, comments, and longer names. For example, replacing userData also changes that part of userDatabase. The same text can mean different variables in different scopes. When it matters which symbol is which, a rename that understands the code (from a compiler or IDE) is better. Replace-all assumes that every match should change. If that is not true, look first and choose a narrower operation.

Remember: unique nearby text selects one match; replace-all selects every exact match in one file.

Continue with tool-use blocks and the complete tool request/result loop.

Request approval without executing the archive

Putting an archive request in a queue is not the same as archiving the document. The document can still be archived later, after an authorized person approves that exact action. The rule is not “never archive.” The rule is “the agent that proposes the archive must not perform it itself.” Two designs keep these steps apart. Choose between them by how complex each is to build, not by whether the word “tool” appears in the answer.

Two valid ways to request approval

Design Agent produces Application does immediately Document state
Structured output An archive proposal as JSON. Validate it and save a pending approval record. Unchanged.
Request-only tool A call to request_archive. The tool's code validates it and saves a pending approval record. Unchanged.

Both designs put a request in a queue. Neither one archives anything right away. The request-only tool does change something: it creates the approval record. But it does not do the restricted action, which is archiving the document. The proposal JSON does not queue itself. Your harness (the application code around the model) must receive it, validate it, and write the record. A tool's name guarantees nothing. Check what its code does and which backend permissions it has.

Structured output is the smaller change, because it adds no tool. That is a preference for simplicity. It does not show that a request-only tool breaks the approval rule. A request tool does not need an MCP server or any MCP server change. It can be an ordinary function in your application whose code only writes the pending record.

sequenceDiagram
    participant M as Proposing agent
    participant H as Harness / request-only tool
    participant DB as Durable approval store
    participant U as Compliance officer
    participant E as Authorized executor
    M->>H: Propose archive of doc-17 version v3
    H->>DB: Save ar-17 as pending_approval
    H-->>M: Request recorded, document not archived
    Note over H,DB: Save run checkpoint if more agent work remains, then release process
    U->>DB: Approve ar-17 through authenticated application
    DB-->>E: Approved request ready for execution
    E->>DB: Verify approval, action, version and execution state
    E->>E: Archive once using request id as idempotency key
    E->>DB: Record completed outcome
What is stored, and what resumes after approval?

The approval queue can be a database table plus a screen for the reviewer. A message queue may notify an executor, but the stored (durable) record holds the decision. This is an illustrative record in your application's database. It is not an LLM API payload:

JSON
{
  "request_id": "ar-17",
  "run_id": "run-84",
  "action": "archive_document",
  "document_id": "doc-17",
  "document_version": "v3",
  "reason": "Retention period reached; awaiting compliance review",
  "status": "pending_approval",
  "approved_by": null,
  "approved_at": null,
  "execution_idempotency_key": "ar-17",
  "execution_status": "not_started"
}

Your application creates the IDs and records the trusted approval details. The model cannot grant approval by writing approved_by. The reviewer approves one specific action on one specific document version. After approval, an executor loads that record again and re-checks authorization and the current conditions. A rejection means no archive. If the document changed, someone must review the approval again; the old approval must not quietly apply to new content. A duplicate notification must not run the operation twice. Store the execution state, use the idempotency (safe-to-repeat) support of the archive operation, and plan recovery after a crash.

You do not need a Python loop that waits while the reviewer is away. If the agent still has conversation work to do afterwards, restore the saved run and add the real execution result, using the way your runtime supports continuing a run. The executor does not need another LLM call just to perform an operation that is already specified and approved. If the proposal was the agent's last task, you do not need to continue the agent at all. Start and resume fundamentals.

Read each candidate design by the timing of the restricted action

Structured proposal: Your application can send a structured proposal to a stored review queue. The real removal of the archive capability belongs in runtime controls and backend authorization. Editing an instruction document alone is not enough.

PostToolUse notification: The notice is sent after the archive tool already ran. So it cannot give approval before archiving. Hooks that run after an action can help with audit logs or notices. They cannot approve an action after it happened.

PreToolUse hook that waits: A check before the action runs is the right place for a gate. The problem is the long wait in this design, not PreToolUse itself. A hook that runs before the action can quickly deny it or ask for permission. A stored workflow can record a pending request and return, instead of keeping a hook open for hours or days. A short permission question in an interactive session is different from a compliance review queue. Do not learn the rule “PreToolUse is always wrong for approvals.” Hook behavior↗.

request_archive tool: This is a valid way to only propose, if two things are true. Its code cannot archive, and the agent cannot get around it with another tool or credential. It respects the same safety line as the structured proposal. Preferring fewer parts does not make this design wrong.

Where enforcement actually lives

allowedTools text written inside CLAUDE.md is not a permission setting; nothing enforces it. In Claude Code, allow rules pre-approve tool use. Removing an allow entry is not the same as denying the tool. Use the runtime's real controls for which tools exist and which are denied. Also limit the backend rights of the service that proposes. Instructions (guidance) describe the behavior you want. Enforcement (settings, hooks, backend rights) stops actions that are not allowed. Permission rules↗.

For example, a custom harness (your own app) exposes read_file, write_output, and request_archive. Its backend identity has no archive permission. A separate executor identity has archive permission, but it accepts only requests that a person approved and that passed validation. The structured-output design uses the same split: it receives proposal JSON instead of exposing a request tool. This is an example of architecture. It does not claim that editing a tool list alone keeps the parts separate.

Decision habit: Find the restricted action. Check whether approval happens before it. Find where the pending record is stored. Find the component that is really allowed to perform the action. Only then compare how complex each design is. Do not decide just because an answer contains the word “hook,” “tool,” or “queue.” Guidance versus enforcement, in the naming-rule example.

Claude Agent SDK (scenario 1 & 4 — embedded agents & developer tooling — territory)

The Claude Agent SDK (it grew out of the Claude Code SDK) gives you the same agent harness as a library that your code can call (TypeScript/Python). You get sessions, tools (Read/Write/Bash/Grep/Glob/WebFetch…), MCP connections, hooks, permissions, and subagents — all of it — to build your own agents outside the terminal. In this runtime, your code sets the options that Claude Code would read from settings files.

Drill-down: continue, resume or fork a session
Want Claude Code CLI Agent SDK (query() options)
The most recent session in this folder --continue / -c (skips -p and SDK sessions unless you run claude -p --continue) Python continue_conversation=True · TypeScript continue: true
One specific session --resume <id or name> / -r (no argument opens a picker) resume=<session_id> · resume: <id>. Take the id from the result message's session_id
Branch off and keep the original unchanged add --fork-session to --resume or --continue: a new session id fork_session=True · forkSession: true

What comes back: the conversation history (with tool calls), the model, and the permission mode (with exceptions). What does not: launch flags such as --mcp-config, --settings, --plugin-dir and --add-dir directories. Pass them again. Settings files are read again at launch. Sessions keep the conversation, not the files on disk: for undoing file changes, the docs point to file checkpointing.

Drill-down: context hygiene features (`/compact`, `/clear`, `/cost`)
  • /compact summarizes the conversation to free up context (use it before a long session runs out). Auto-compaction warns you as the window fills.
  • /clear starts the session fresh. Memory files stay. That is why long-lasting knowledge belongs in CLAUDE.md, not in the chat.
  • /cost shows token use. Architect habit: watch it in long agent sessions.
  • Extended thinking when you ask for it — people often trigger it with phrases like "think hard" (as of mid-2026 — check current docs for the supported control). Use it for complex planning steps.
Drill-down: model selection inside Claude Code

/model or a setting fixes the model. The architecture answer follows model tiering from Domain 1 (match how hard the task is to the model's ability and cost). Use the strongest model (Opus-class) for planning and architecture. Use faster models (Sonnet/Haiku-class) for many simple edits. In CI, pin an exact model version so runs can be repeated with the same result.