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 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.
‹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:
Managed settings: managed-settings.json, MDM, or the claude.ai console. Nothing in your own files or in --settings overrides a managed key.
Command line: claude --settings '<json or file>', for this session only.
Project local: .claude/settings.local.json.
Shared project: .claude/settings.json.
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):
Some model keys don't merge: fallbackModel (taken whole from the highest file), modelPicker (only managed, --settings or user count), availableModels (a managed list applies as-is; your additions are ignored), modelSettings (resolved per model).
A few security keys honor the stricter value even from a lower level, over managed. Examples: disableClaudeAiConnectors: true, permissions.blockReadsOutsideWorkingDirectories: true, isolatePeerMachines: true, a lower maxEffortLevel cap. The full table also covers enableArtifact, autoMode.classifyAllShell, remoteControlAtStartup, crossSessionInbound, useAutoModeDuringPlan, syncClaudeAiSkills and syncClaudeAiPlugins.
Some keys are managed-only. In user or project files they do nothing. Examples: allowManagedPermissionRulesOnly, allowManagedHooksOnly, allowManagedMcpServersOnly, strictPluginOnlyCustomization, managedMcpServers, claudeMd.
permissions.defaultMode values auto and bypassPermissions don't take effect from project or local files. Set them in user or managed settings.
Environment variables are not a level. For example, ANTHROPIC_MODEL in your shell beats the model key from any file.
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.
Subagents with the same name: managed > --agents flag > project > user > plugin.
Skills with the same name: enterprise > personal > project. Yes, here your personal skill beats the project skill, the opposite of settings.
MCP servers with the same name: local > project > user > plugin > claude.ai connectors. A server the org provides through managedMcpServers beats a local, project or user server with the same name. The whole entry from the winner is used; fields don't merge. And managed-mcp.json is different: it doesn't win a contest, it ends it. When that file exists, Claude Code loads only its servers (plus any from managedMcpServers), and users can't add others.
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).
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.
📄 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).
📄 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.
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.
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.jsonplus 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.
📄 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.
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.
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
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.
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.
Trap 1: a louder prompt, when the question asks for a guarantee. The question says "must never" or "guarantee". One answer choice says "add CRITICAL to CLAUDE.md" or "repeat the rule in the system prompt". Don't pick it. Capital letters and repetition make the instruction louder, but it is still text that the model reads, and the model can still ignore it. So it is not a guarantee. Pick the choice that adds a check in code: a deny rule or a hook in Claude Code, or a code check in your API app.
Trap 2: a hook, when the question only asks Claude to know something. The question says the team wants Claude to know a convention, for example "use pnpm". One answer choice adds a hook or a code check. Don't pick it. A hook can block an action or run a command, but it can't teach Claude anything. Pick the choice that adds a line to the project CLAUDE.md.
Check yourself: guidance or enforcement?
"The team wants Claude to use pnpm, not npm, in this repo." → Guidance: one line in the project CLAUDE.md.
"Compliance requires that Claude can never read files under secrets/." → Enforcement: permissions.deny: ["Read(./secrets/**)"], ideally in managed settings.
"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.
"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.
"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.
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
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
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.
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.
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.
Claude Code loads these files automatically when the session starts. For memory, all loaded files are combined into one set of instructions. The narrower file (project, then nested) is meant to guide behavior for its part of the folder tree. This is only a weighting of context. It is not the strict, fixed > chain that decides settings and permissions (that strict chain is explained under Permissions below). When instructions conflict, do not assume that the more specific one will reliably win. Remove the conflict. For rules that must be enforced, add a check that runs code.
What belongs there: build and test commands, architecture conventions, style rules, and repo habits. These are long-lasting instructions, not prompts for one task.
Keep it short, and write commands ("Run the tests"), because every token loads into every session. Do not paste long documents into it. Instead, use @path imports: an @ followed by a file path (e.g. @docs/architecture.md) adds that file to the loaded instructions. The imported file still uses context space.
In Claude Code, # at the start of a message adds a memory quickly. /memory edits memory files. /init creates a first CLAUDE.md for a repo.
🎯 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:
[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.
[~/.claude/CLAUDE.md — user] — the engineer's personal file: "Answer concisely. I prefer vitest over jest." It goes with the engineer to every repo.
[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.
[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.
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"
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 PreToolUsehook
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 PostToolUsehook
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
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.
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.
The model sees the team convention, but it still writes class invoice_service.
A configured Java linter checks the new code against the team's class-name pattern and reports a violation.
Someone must fix the change before a required acceptance check can pass. Claude can write the fix, or a person can.
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
🏭 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
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:
Copying the convention into every user's CLAUDE.md spreads more text that people must maintain. It still depends on the model following instructions. It is a weak way to share a team standard.
Moving it into CLAUDE.local.md moves a shared convention into a personal file. “Local” does not give any enforcement power. Adding instructions later does not turn them into a checker.
Leaving it in the repository-root CLAUDE.md because it “wins” is the right place to document the rule, but for the wrong reason. Nothing about that location guarantees that it beats personal text. Keep the team instruction, and add a check that runs code.
Settings that register a hook which runs a real validator is the enforcement path. Settings must configure a supported control, and the hook must run a real checker. No general settings key means “all Java names obey this sentence.” For strong acceptance, add the same checker to the required CI checks.
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.
“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
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
In Claude Code, permission rules decide whether a tool call runs. They are listed in .claude/settings.json under permissions.allow / permissions.deny. Read the Tool(pattern:*) syntax as "this tool, but only for calls that match this pattern". Bash(npm run test:*) means the Bash tool, limited to commands that start with npm run test. * is the wildcard.
To deny a pattern, put it in the deny list. Claude Code reads this file at start-up and enforces it in every session. For example, this blocks Claude from reading sensitive files:
Real file: cookbook:skills/.claude/settings.json — a real project settings file (it uses the old hook format; see the note below).
Precedence (strongest first): enterprise managed policy > CLI flags > local project settings > shared project settings > user settings.
🧭 Why this order confuses people who know CSS: the lower four layers ARE a specificity cascade (the layer closest to this moment wins — user < project < local < CLI). But the top layer is a hard limit, not a default. Managed policy always beats specificity. Specificity wins — until policy sets a limit. And the limit applies per KEY, never to a whole layer. If managed policy sets a single-value key, policy owns that key completely (lower layers cannot choose a different value). Permissions can only get stricter. Keys that policy does not set cascade normally. So in one Claude Code run, your CLI flag can win model while permissions stays limited by policy. For a layer-by-layer explanation, see the settings-precedence card. Watch the merge in the diff-style cascade.
--dangerously-skip-permissions exists for containers and CI. The exam answer is to use narrow allowlists instead wherever possible.
Protecting sensitive files: put secrets (.env, keys) on the deny list, so Claude Code blocks every read of them.
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.
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.
# 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.
#!/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.
💡 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.
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.
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: 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.
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.
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).
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.
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:
Topic files: split the rules into .claude/rules/testing.md, api-conventions.md, deployment.md. This keeps things organized without one giant file.
Path-scoped rules: give a rules file YAML frontmatter with glob patterns, such as paths: ["terraform/**/*"] or paths: ["**/*.test.tsx"]. Then it loads only when Claude edits a matching file. Conventions that do not apply stay out of the context, so tokens go to the rules that apply right now.
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.
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:
Plan mode — for tasks that affect the architecture: large changes, many files, several valid approaches, or decisions about service boundaries. Claude explores the codebase and designs an approach before it makes changes. This avoids expensive rework when you discover dependencies in the middle of editing. Typical cases: splitting a monolith into microservices, a library migration that touches 45+ files, or choosing between ways to integrate.
Direct execution — for small, clear, well-understood changes: a bug fix in one file with a clear stack trace, or adding one validation check. Plan mode here is only extra ceremony.
How to turn it on: Shift+Tab (cycles default → acceptEdits → plan), /plan before one prompt, claude --permission-mode plan, or "defaultMode": "plan" in .claude/settings.json. Claude reads files and runs shell commands to explore (a command outside the read-only set asks first), but it does not edit your source until you approve the plan. The blocks also hold in -p runs and SDK sessions.
The combination is valid and appears on the exam: plan mode to investigate and design, then direct execution to build the approved plan.
The Explore subagent helps you save context. It keeps long discovery output (mapping a large codebase, tracing flows) in its own context and returns only a summary. So a task with several phases does not fill the main window with exploration output.
🎯 "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.
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
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.
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:
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)
Headless mode (Claude Code with no interactive UI): claude -p "review this diff" --output-format stream-json works in any pipeline. Exit codes and JSON output let scripts use it.
GitHub Actions integration: @claude mentions on PRs and issues. The action runs Claude Code on the repo, with a token of limited scope and your CLAUDE.md conventions.
What the exam asks an architect to think about: tokens with the smallest needed rights, pinning the action and model version, a budget limit per run, and whether Claude's PR feedback is an advisory gate or a blocking gate. A blocking gate needs criteria that give the same result every time (tests, lint), not a feeling. Also: an audit log of what the agent did.
Headless + hooks + allowlists = the reason you can trust CI. The agent can run only allowed commands, every action is logged, and formatting and tests run the same way after every edit.
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)
Install the GitHub CLI and run gh auth login.
Open claude in the repo and run /install-github-app. It works with github.com repos only.
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).
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)
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.
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.
Copy examples/claude.yml from the action's repo into .github/workflows/claude.yml.
Test it: write @claude in an issue or PR comment.
Rolling out to a whole organization
Install the Claude GitHub App once at the org level, for all repos or a chosen list.
Store the secret as an org-level Actions secret. Use a Console API key here, not an OAuth token, because an OAuth token is tied to one person's subscription.
Add the workflow file to each repo, or write it once as a reusable workflow that each repo calls.
No long-lived secret at all: use workload identity federation (GitHub OIDC token → Claude Console service account), with anthropic_federation_rule_id and anthropic_organization_id, plus id-token: write.
📄 .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.
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.
--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)
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
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:
The layers, in CI: the runner clones the repo. So the projectCLAUDE.md (guidance), .claude/settings.json (enforcement), rules, and skills control the run, and there is no user layer. This is why teams commit their memory and settings: on GitHub, only the repo's layers speak.
Security uses the same three actors as always: The permission check on the person who comments (write access is required by default) is the gate against random prompt injection through public comments. The API key lives in secrets. And the model still merges nothing; branch protection is the gate that always behaves the same way.
Tell the two CI forms apart: @claude = interactive, started by a person, shaped like a conversation (a teammate in the thread). The headless claude -p job above = automated, started by an event, shaped like a pipeline. “The team wants to ask Claude to fix review feedback from the PR page” → the tag. “Every merge must regenerate release notes” → headless. Different triggers, same harness.
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).
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.
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.
GitHub starts a runner: an empty virtual machine (runs-on: ubuntu-latest). It puts the secret (ANTHROPIC_API_KEY) into the job's environment.
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.
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.
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).
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).
GitHub deletes the runner.
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):
# 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.
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
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.
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.
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)
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.
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.
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.
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:
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)
// .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
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.
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.
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.
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.
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.
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.
// 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:
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.
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.
// 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:
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:
Grep, then twelve targeted Edit calls. This works, and Grep is not the mistake. The waste is twelve separate change calls when every match needs the same replacement. “Each occurrence has unique nearby text” explains how to make each call clear. It does not explain why twelve calls are needed.
Bash running sed. This also works. Bash is a built-in tool, so “sed is wrong because Bash is not built-in” is a false reason. The cost is shell replacement syntax that must fit the platform, when a tool made for editing already exists.
Edit with replace_all: true. This says the intended scope directly: one file, one old string, one new string, every intended match. There may still be Read calls and check calls. “One call” means one change call, not always one whole agent turn.
Write with the whole file. This rewrites the file without need. Write fits when you want to give the complete file content. A small replacement needs less content and gives fewer chances for unrelated edits by mistake. Edit takes old and new strings; it does not take a unified-diff format.
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.
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:
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.
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.
Use the SDK when you put an agent inside a product (support bot, pipeline worker). Use Claude Code interactively for development work itself.
The built-in tools (Read, Write, Edit, Bash, Grep, Glob) ARE the "developer productivity tooling" of scenario 4. Know what each one does. Grep/Glob only search and read; Bash is the powerful one that permissions guard.
Sessions keep the conversation state. --continue/--resume (CLI) and session ids (SDK) continue earlier work.
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)
--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.