Configuring and governing Claude's agent harness: the agentic loop, permission modes and allow/ask/deny rules, settings, the CLAUDE.md hierarchy and path-scoped rules, deterministic hooks, isolated and fork subagents, progressive-disclosure skills and custom slash commands, plugins and marketplaces, MCP scopes and policy filtering, headless CI runs and automated code review, plan mode and built-in tool choice, team rollout with managed settings, and Claude Code on Vertex AI.
Test Yourself: Claude Code Practice Quiz →Claude Code is Anthropic's agentic coding tool. It reads your codebase, edits files, runs commands, and verifies its own work through a tool-using loop. The same harness ships as a library, the Claude Agent SDK, and Claude Managed Agents runs a comparable loop on Anthropic's infrastructure; both are covered in the separate Claude Agent SDK guide.
Every task cycles through three blended phases: gather context (search, read) → take action (edit, run) → verify results (tests, diffs), repeating until done. Each tool result informs the next step, and you can interrupt at any point. Hooks, skills, subagents, and MCP all plug into specific phases of this loop.
CLAUDE.md, .claude/rules/, and auto-memory MEMORY.md load; Claude searches and reads files, or delegates noisy exploration to a subagent.
PreToolUse hooks run first → deny → ask → protected/critical-path checks → allow → active permission mode → PermissionRequest hooks → prompt or SDK canUseTool.
Edits, shell commands, MCP tool calls, or skill invocations execute (optionally inside OS-level /sandbox isolation).
PostToolUse / FileChanged hooks format or audit; Claude runs tests and reads results before Stop hooks evaluate completion.
--worktree / -w): creates an isolated checkout under .claude/worktrees/<name> sharing git history on your machine; does not copy gitignored files (node_modules, .env, venv), requiring dependency setup or a SessionStart hook.claude.ai/code) & Teleport& <prompt> to dispatch a background cloud session. Use /teleport (/tp or claude --teleport <id>) for one-way Cloud → Local terminal handoff (requires clean local git state; local sessions cannot be pushed to web).Claude Code goes beyond a shallow single-loop ReAct agent by combining four architectural pillars mapped to the four context engineering strategies (Write, Select, Compress, Isolate):
TodoWrite / TodoRead) [Write]: Externalizes a live checklist in agent state so multi-step plans survive context window pressure and tool failures without goal drift.Task / Agent) [Isolate]: Spawns child agents in fresh context windows for noisy search or independent checks, returning only a distilled summary to the parent.Glob (paths), Grep (ripgrep regex), and paginated Read(file_path, offset, limit), plus writing durable state to disk (CLAUDE.md, SPEC.md, MEMORY.md)./compact (or /clear) keep active context inside the high-recall attention budget.The model proposes; the harness disposes. Anything written in CLAUDE.md or .claude/rules/ is advisory guidance the model weighs. Anything in settings.json permission rules, hooks, or managed policy is enforced deterministically by the client regardless of what the model decides.
Claude Code as a tool: configuration, workflows, CI and rollout. Building your own agents (the agent loop, stop_reason, multi-agent orchestration, SDK hooks and sessions, tool-error handling, escalation, provenance) and Claude Managed Agents are in the Claude Agent SDK guide.
A permission mode sets the session's baseline for what runs without prompting. Rules then carve specific tools, paths, or commands in or out. Set the starting mode with --permission-mode or permissions.defaultMode (interactive CLI defaults to auto on supported plans in v2.1.283+; project .claude/settings.json and .claude/settings.local.json ignore defaultMode: "auto" and "bypassPermissions").
| Mode | Runs without asking | Still prompts / blocked | Use when |
|---|---|---|---|
default (manual) | File reads + built-in read-only shell commands (ls, pwd, git status/diff/log, rg, find without -exec/-delete) in working dirs | Every file edit, mutating shell command, and path outside working directories | Sensitive repos; reviewing every mutating action |
acceptEdits | Everything in default + CWD file edits and basic file-manipulation utilities (sed, cp, mv, mkdir, rmdir, touch, rm) | Other Bash commands, paths outside CWD, protected repo paths, and critical-path rm | Iterating on code you are reviewing |
plan (Shift+Tab / /plan) | File reads, read-only shell commands, and read-only subagents (Explore, Plan) | All file edits and mutating commands until plan approval (PreToolUse "allow" does not bypass plan mode's edit block) | 4-step Spec-Driven workflow: Explore → Ask clarifying questions → Draft spec → Execute after approval |
auto | Actions approved by a background safety classifier; strips broad allow rules (Bash(*), Bash(python*), Bash(node*), Agent) on entry | Classifier-flagged actions (curl-pipe-sh, prod deploys, force push, mass delete), protected paths, deny/ask rules; falls back to default if prerequisites fail | Long interactive tasks where prompt fatigue is the problem (disable org-wide via "disableAutoMode": "disable") |
dontAsk | Reads in working directories and explicitly pre-approved (allow) tools | Anything that would prompt is silently denied (PermissionRequest hooks and SDK canUseTool are skipped) | Locked-down CI, scripts, and background jobs |
bypassPermissions | All tools and commands except the hard guardrails in the next column | Explicit deny rules, explicit ask rules, PreToolUse hook denials, protected repo paths, and critical-path rm/rmdir | Isolated containers and disposable VMs only (disable org-wide via "disableBypassPermissionsMode": "disable") |
acceptEdits + protected pathsacceptEdits also pre-approves CWD shell file utilities (sed, cp, mv, rm) — pair it with deny: ["Bash(rm *)"] when deletions must stay gated. Conversely, writes to protected repo paths (.git, .claude, .vscode, .idea, .husky, .mcp.json, shell profiles) are checked before permissions.allow, so putting Edit(.claude/**) in allow never pre-approves writes there, and even bypassPermissions still prompts on them.
PreToolUse hooks → deny → ask → protected/critical-path checks → allow → active mode → PermissionRequest hooks → prompt / canUseTool. First match in deny → ask → allow wins regardless of specificity.deny: Bare tool names in deny ("Bash", "WebFetch", "mcp__*", "Agent(Explore)") remove the tool from context entirely; scoped rules ("Bash(rm *)") keep the tool in context and block matching calls.Read & Edit only: Only Read(...) and Edit(...) accept path globs. Edit(...) governs Edit, Write, and NotebookEdit — Write(path) and Glob(path) rules never match!//path = absolute filesystem root (Read(//etc/**)); ~/path = $HOME; single /path = relative to <cwd> in project/local settings or ~/.claude/ in ~/.claude/settings.json (not root!); ./path or path = CWD.Read(path) in deny also blocks Edit/Write and @file prompt mentions; Edit(path) in allow also grants Read. WebFetch(domain:*) in allow also opens all network hosts to sandboxed Bash!/sandbox)mcp__<server>__<tool> targets one tool; mcp__<server> or mcp__<server>__* targets all tools on that server; mcp__github__get_* wildcards the tool suffix.(...) on mcp__ rules in settings files (skipped with a startup warning), and unanchored "*" or "mcp__*" in allow is skipped.sandbox.enabled: true; Seatbelt on macOS, bubblewrap on Linux/WSL2) restricts child process filesystem and network access.sandbox.autoAllowBashIfSandboxed: true, sandboxed Bash commands run without prompting. Failed sandbox commands may retry with dangerouslyDisableSandbox: true (prompting the user) unless "allowUnsandboxedCommands": false enforces strict sandboxing.{
"permissions": {
"defaultMode": "acceptEdits",
"allow": ["Bash(npm run *)", "Edit(/src/**)", "mcp__github__get_*"],
"ask": ["Bash(git push *)"],
"deny": ["Read(./.env)", "Read(//etc/**)", "Bash(rm *)", "mcp__github__delete_repo"]
}
}
Bash patterns match command prefixes, so deny: ["Bash(git push *)"] does not match git -C . push. For a hard boundary, combine deny rules with a PreToolUse hook and OS-level sandboxing (sandbox.enabled: true).
| Precedence (high → low) | Where | Who it affects | Typical content |
|---|---|---|---|
| 1. Managed | Server-managed, MDM/OS policy, or managed-settings.json (+ managed-settings.d/*.json) | Everyone the org deploys to; nothing below overrides a managed key | Security policy, compliance, model/plugin/MCP locks |
| 2. Command line | --settings <file-or-json> and CLI flags (--permission-mode, --allowedTools) | This session only | One-off overrides, CI flags |
| 3. Project local | .claude/settings.local.json (always at git repo root, even from subdirs/worktrees) | You, this repository (gitignored) | Personal overrides, local testing |
| 4. Shared project | .claude/settings.json | Everyone who clones the repo | Team permissions, hooks, plugins, env |
| 5. User | ~/.claude/settings.json | You, every project | Personal defaults and rules |
Arrays merge across layers; scalars override. Permission rule arrays from every layer combine (deny → ask → allow), so a deny at any layer still blocks. A scalar model in managed settings is only a starting default; the hard locks are availableModels (allowlist) and deniedModels (denylist, which wins over availableModels).
| Memory / instruction source | Location & loading behavior | Shared with |
|---|---|---|
Managed policy CLAUDE.md | OS system path (/Library/Application Support/ClaudeCode/CLAUDE.md on macOS, /etc/claude-code/CLAUDE.md on Linux/WSL, C:\Program Files\ClaudeCode\CLAUDE.md on Windows) or "claudeMd" string in managed-settings.json; cannot be excluded | Whole organization |
User CLAUDE.md & rules | ~/.claude/CLAUDE.md and ~/.claude/rules/*.md (loaded at session start before project files) | Only you, all projects |
Project CLAUDE.md & AGENTS.md | ./CLAUDE.md or ./.claude/CLAUDE.md (plus AGENTS.md when active); discovered upward from CWD and loaded in root-to-CWD order at launch. Subdirectory CLAUDE.md files load lazily on demand when files in that subtree are read/edited | Team, via source control |
Path-scoped .claude/rules/*.md | Discovered recursively in .claude/rules/. Rules without paths: load at launch; rules with paths: ["src/api/**/*.ts"] load on demand when matching files are read | Team, via source control |
Local CLAUDE.local.md | ./CLAUDE.local.md (gitignored; appended after CLAUDE.md in each directory) | Only you, this project |
Auto memory (MEMORY.md) | ~/.claude/projects/<project>/memory/MEMORY.md (plus topic files). Claude writes notes itself; first 200 lines or 25 KB of MEMORY.md load at startup (toggle via /memory, "autoMemoryEnabled": false, or CLAUDE_CODE_DISABLE_AUTO_MEMORY=1) | Only you, per git repo (shared across worktrees) |
/init & monorepo rulesCLAUDE.md: longer files cost context and reduce adherence./init & # <rule> shortcut: /init scans the repo to bootstrap a starter CLAUDE.md (treat as a draft to prune); starting a prompt with # <rule> appends an instruction to your chosen CLAUDE.md tier.@path/to/file imports expand at launch (max 4 hops) and do not save context tokens."claudeMdExcludes": ["**/monorepo/CLAUDE.md"].--add-dir loads skills/commands/agents from extra directories, but not CLAUDE.md or .claude/rules/ unless CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD=1./doctor prompt-audit and inspect token usage with /context. (Note: .claudeignore does not exist — use permissions.deny: ["Read(...)"].)AGENTS.md & .claude/rules/CLAUDE.md lazy-load trap: When launched at repo root, frontend/CLAUDE.md does not load at startup — it loads lazily only when Claude reads/edits inside frontend/. Use .claude/rules/frontend.md with paths: ["frontend/**"] for deterministic glob-triggered rules.AGENTS.md + CLAUDE.local.md trap: Under default auto project instructions, creating CLAUDE.local.md switches detection to claude-md only and silently stops loading AGENTS.md unless /config → Project instructions is set to claude-md-and-agents-md..claude/rules/*.md files are discovered recursively; scope comes only from paths: YAML frontmatter. A file in .claude/rules/db/ without paths: loads at launch for every task./clear vs /compact/compact summarizes the current conversation — which preserves poisoned assumptions inside the summary! When a session suffers from context poisoning or switches to an unrelated task, run /clear (or /rewind prior to the bad turn).Bash outputs crowd the attention window; fix by delegating exploration to subagents and using /compact <focus> when switching phases.CLAUDE.md files or multi-turn requirements cause unpredictable adherence; audit with /doctor prompt-audit./output-style) & Status Line (/statusline)/output-style (modifies system prompt): Unlike CLAUDE.md (appended as a user message after the system prompt), /output-style directly replaces or swaps Claude Code's system prompt. Built-ins: Default (standard SWE), Explanatory (educational "Insights" callouts), and Learning (collaborative mode that leaves TODO(human) markers). Custom styles in ~/.claude/output-styles/*.md or .claude/output-styles/*.md strip default SWE instructions unless frontmatter sets keep-coding-instructions: true (saved to .claude/settings.local.json as outputStyle)./statusline (zero-token UI telemetry): Configures a shell command in settings.json ("statusLine": {"type": "command", "command": "..."}) that receives live session JSON on stdin (model, workspace, cost, and transcript_path pointing to ~/.claude/projects/<encoded-cwd>/<uuid>.jsonl) and renders a terminal footer at zero LLM token cost.MCP "local scope" ≠ local settings. A local-scoped MCP server lives in ~/.claude.json under the project entry, while local settings live in .claude/settings.local.json at the git repository root.
@ Imports & Path-Scoped Rules| Instruction is for… | Put it in | In git? | Trap |
|---|---|---|---|
| The whole team, every task | ./CLAUDE.md or ./.claude/CLAUDE.md | Yes | Keep it short (under ~200 lines); move topic detail into imports or rules |
| Only you, every project | ~/.claude/CLAUDE.md | No | Teammates never receive it: team conventions placed here "work on my machine" only |
| Only you, this repo | ./CLAUDE.local.md | No (gitignored) | Not shared across worktrees; import @~/.claude/<file>.md instead to reuse personal notes |
| One package or directory | <subdir>/CLAUDE.md | Yes | Loads lazily, only once Claude reads files in that subtree |
| One file type spread across many directories (tests, API handlers) | .claude/rules/<topic>.md with paths: globs | Yes | A directory CLAUDE.md cannot target "every *.test.tsx anywhere" |
| The whole org, not excludable | Managed policy CLAUDE.md | IT-deployed | Users cannot override or exclude it |
@ imports (modular CLAUDE.md):
# CLAUDE.md See @README.md for the project overview and @package.json for npm scripts. ## Standards - Testing conventions: @docs/testing.md - API conventions: @docs/api-conventions.md - My personal overrides: @~/.claude/my-project-instructions.md
~ paths are allowed.CLAUDE.md as a short index of @imports, so each topic file has one owner and only relevant standards are pulled in per package.Path-scoped rules (.claude/rules/*.md):
---
paths:
- "src/api/**/*.ts"
- "**/*.test.{ts,tsx}"
---
# API & test conventions
- Validate every request body with the shared schema helpers.
- Tests use the factories in test/factories; never hit the real network.
paths load at launch like CLAUDE.md. Rules with paths load only when Claude reads a matching file, so they cost no tokens on unrelated tasks.** and brace expansion (*.{ts,tsx})./memory: it lists every CLAUDE.md, CLAUDE.local.md and rules file loaded in the current session. Run it first when "Claude ignores my instruction".(1) A new teammate's Claude ignores the team's conventions → they live in someone's ~/.claude/CLAUDE.md (user scope), not the project file. Move them to ./CLAUDE.md or .claude/rules/. (2) Test files sit next to source files across 30 directories → one rule with paths: ["**/*.test.*"], not 30 directory CLAUDE.md files. (3) Splitting a big CLAUDE.md into @imports does not reduce tokens; path-scoped rules do.
Ask two questions: must it hold 100% of the time? (then settings rules or a hook, never prose) and does it matter for every task? (then always-on; otherwise on-demand). Pick the lowest-context mechanism that satisfies both.
| You need… | Use | Why not the alternatives | Context cost |
|---|---|---|---|
| Conventions for every task (test command, build steps) | CLAUDE.md | A hook can't express style; a skill may not trigger | Every session; dilutes past ~200 lines |
Guidance only for src/db/** | .claude/rules/db.md with paths: | In CLAUDE.md it wastes tokens on unrelated tasks | Only when matching files are touched |
Block a tool, path, @file mention, or command prefix, always | permissions.deny (managed if org-wide) | CLAUDE.md is advisory; PreToolUse skips @file mentions | Zero (bare tool deny even removes tool schema) |
| Inspect/rewrite args or block dynamically before a tool runs | PreToolUse hook (exit 2 or permissionDecision: "deny") | Permission rules match globs/prefixes, not payload content | Near-zero (only injected reason/context) |
| Deterministic side effect after edits (formatter, linter, audit) | PostToolUse hook (or FileChanged if edited via Bash) | Instructions get skipped; PreToolUse fires before disk write | Near-zero |
| Wide codebase search or independent parallel check | Subagent (.claude/agents/) | Main context fills with throwaway file reads | Isolated window; returns summary only |
Repeatable workflow or slash playbook (/release) | Skill (SKILL.md; add disable-model-invocation: true for side effects) | In CLAUDE.md you pay for the full playbook every session | Description only (or zero if manual-only) until invoked |
| Replace the default SWE persona (e.g. tutor, PM, security architect) | Output style (/output-style or .claude/output-styles/*.md) | CLAUDE.md adds to the SWE system prompt instead of replacing it | Replaces default system prompt tokens |
| Live model, branch, or token/cost HUD in the terminal | Status line (/statusline in settings.json) | Asking Claude or running hooks wastes tokens/turns | Zero (runs out-of-band on stdin JSON) |
| Multi-step task & spec tracking across context compactions | TodoWrite / TodoRead + on-disk SPEC.md | Conversational plans get lost or distorted during /compact | Minimal structured state / on-demand file read |
| Ship skills, agents, hooks & MCP across many repos | Plugin via a marketplace | Copy-pasted .claude/ folders drift across repos | Per bundled component |
| Org policy users cannot override | Managed settings (+ managed CLAUDE.md for guidance) | Project/user files can be edited locally | Zero |
CLAUDE.md & .claude/rules/ — Additive: Every matching file across all layers loads into context simultaneously.Permissions & Hooks — Merge & most-restrictive wins: Permission rules merge across all settings files (deny > ask > allow). Hooks from every active layer register and execute in parallel on matching events; PreToolUse decisions resolve by deny > defer > ask > allow.Subagents, Skills, & MCP servers — Override by name: Only one definition wins when names collide (plugins use plugin:name namespaces). Precedence trap: Subagents resolve managed > --agents > project > user > plugin (project beats user), whereas Skills resolve managed > user > project > nested > plugin > synced (user beats project), and MCP servers resolve local > project > user.plan mode): Start in plan mode (Shift+Tab) on Opus to explore the repo, resolve ambiguities via clarifying questions, and write a comprehensive specification to disk (SPEC.md)./clear + Sonnet + @SPEC.md): Run /clear to wipe exploration noise, switch to Sonnet (/model sonnet) for fast, cost-effective execution, and pass @SPEC.md as the clean source of truth (pairing UI tasks with Playwright MCP for visual self-verification).A lifecycle handler configured in settings.json (user, project, local, or managed policy), a plugin's hooks/hooks.json, or a skill/subagent YAML frontmatter (hooks:). Hooks receive event JSON on stdin (session_id, cwd, hook_event_name, plus tool fields tool_name, tool_input).
"command" (shell/exec script; supports "async": true for background side effects), "http" (POSTs event JSON to a URL), "mcp_tool" (invokes a configured MCP server tool), "prompt" (single-turn fast LLM check returning {"ok": true|false, "reason": "..."}), and "agent" (multi-turn verifier subagent with Read/Grep/Glob, up to 50 turns).matcher, or setting "matcher": "" or "*", acts as a wildcard matching every tool call. Because synchronous hooks block the agentic loop on every match, scope matchers tightly ("Edit|Write") or set "async": true on non-blocking telemetry/logging commands.| Exit code | Meaning | Effect on the event |
|---|---|---|
0 | Success / allow | Event proceeds. stdout is parsed for JSON decision fields (hookSpecificOutput, decision, continue). Plain-text stdout is added to Claude's context only on SessionStart, Setup, UserPromptSubmit, and PostModelSwitch. Exit 0 without JSON does not bypass permission prompts. |
2 | Blocking error | Blocks the action (on blockable events). stderr is fed back to Claude as the block reason. Valid JSON on stdout is still read but can't override the block (even "allow"); a JSON blocking-decision reason, if present, replaces stderr as the message. |
1 or other non-zero (no valid JSON) | Non-blocking error (fail-open default) | Exam trap: exit 1 without valid hook JSON logs stderr as a warning and lets the tool call proceed — unless "onFailure": "block" is set on the command/http hook (which turns non-zero exits, non-2xx HTTP responses, and timeouts into blocks). (If stdout contains valid hook JSON, Claude Code applies the JSON decision and ignores the exit code; WorktreeCreate/WorktreeRemove fail on any non-zero exit.) |
| Event | Fires when | Matcher | Exit 2 / JSON block effect |
|---|---|---|---|
PreToolUse | Before a tool call executes in every mode (skips @file prompt mentions) | Tool name (Bash, Edit|Write, mcp__srv__tool) + optional if ("Bash(rm *)") | ✅ Blocks tool call; stderr or permissionDecisionReason sent to Claude |
PermissionRequest | Before a permission dialog shows (or before -p/background auto-deny outside dontAsk) | Tool name | ✅ Denies via hookSpecificOutput.decision.behavior: "allow"|"deny" |
PostToolUse | Immediately after a tool succeeds (Edit|Write misses Bash file edits) | Tool name | ❌ Cannot undo execution; feeds stderr / decision: "block" reason to Claude or redacts output via updatedToolOutput |
PostToolUseFailure | After a tool execution throws or exits non-zero (not permission/schema rejections) | Tool name | ❌ Feeds stderr / additionalContext to Claude alongside the tool error |
UserPromptSubmit | User submits a prompt, before Claude processes it | (none) | ✅ Blocks prompt processing and erases the prompt from context |
Stop / SubagentStop | Main agent (Stop) or subagent (SubagentStop) finishes its turn | (none) for Stop; agent name for SubagentStop | ✅ Prevents stopping and resumes work (check .stop_hook_active on stdin; hard cap of 8 consecutive continuations) |
SubagentStart | Subagent spawns or resumes | Agent name (Explore, custom) | ❌ Cannot block spawn; injects additionalContext into the subagent |
SessionStart / Setup | Session starts (SessionStart) or --init/--maintenance runs (Setup) | startup|resume|clear|compact (SessionStart); init|maintenance (Setup) | ❌ Cannot block; stdout / additionalContext added to context; $CLAUDE_ENV_FILE available to persist export vars |
PreCompact | Before context compaction (PostCompact fires after) | manual|auto | ✅ Blocks compaction (exit 2 or decision: "block") |
ConfigChange | Settings, managed policy, or skill file changes mid-session | user_settings|project_settings|local_settings|policy_settings|skills | ✅ Blocks config change from taking effect (except policy_settings, which cannot be blocked) |
FileChanged / CwdChanged | Watched file changes on disk (Edit, Bash, or external) / directory changes | Literal |-separated filenames (.envrc|.env) for FileChanged | ❌ Cannot block; catches Bash file edits missed by PostToolUse(Edit|Write); $CLAUDE_ENV_FILE available |
WorktreeCreate / WorktreeRemove | --worktree or isolation: worktree subagent creates/removes worktree | (none) | ✅ Replaces default git worktree (e.g. SVN/Mercurial); WorktreeCreate prints path on stdout (any non-zero exit fails) |
Notification / SessionEnd | User alert (permission_prompt, idle_prompt) / session close (1.5s timeout) | Notification type / exit reason | ❌ Cannot block; alerting and cleanup only |
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"if": "Bash(rm *)",
"onFailure": "block",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/no-rm.sh"
}]
}]
}
}
#!/bin/bash cmd=$(jq -r '.tool_input.command') if [[ "$cmd" == rm* ]]; then echo "Blocked: rm is not allowed" >&2 exit 2 # block; stderr goes to Claude fi exit 0 # no decision; normal flow
stdout (exit 0)PreToolUse (hookSpecificOutput): permissionDecision resolves across parallel hooks by "deny" > "defer" > "ask" > "allow". "deny" blocks in every mode including bypassPermissions; "defer" pauses headless -p/SDK runs with stop_reason: "tool_deferred" for later --resume; "allow" bypasses normal prompts (unless matched by permissions.deny/ask or blocked by plan mode).updatedInput & additionalContext: updatedInput replaces the entire tool_input object (omitted keys are dropped; permission rules re-evaluate against the rewritten input). additionalContext must be nested inside hookSpecificOutput alongside hookEventName.PermissionRequest vs PostToolUse/Stop: PermissionRequest uses hookSpecificOutput.decision.behavior ("allow"|"deny"); PostToolUse, Stop, UserPromptSubmit, PreCompact, and ConfigChange use top-level {"decision": "block", "reason": "..."} (plus updatedToolOutput on PostToolUse).@file bypass, portable paths & $CLAUDE_ENV_FILE@file mention bypass trap: PreToolUse on Read does not fire when a user types @.env in their prompt (Claude Code inlines @file directly without calling Read). Block both with permissions.deny: ["Read(./.env)"] (or CLAUDE_CODE_DISABLE_FILE_MENTIONS=1).${CLAUDE_PROJECT_DIR} in repo hooks and ${CLAUDE_PLUGIN_ROOT} (code) / ${CLAUDE_PLUGIN_DATA} (persistent state) in plugin hooks.$CLAUDE_ENV_FILE): Append export KEY=VAL to $CLAUDE_ENV_FILE inside SessionStart, Setup, CwdChanged, or FileChanged hooks to persist env vars across subsequent Bash calls.Specialized assistants with their own context window, system prompt, tool set, and optional model/permission mode.
.claude/agents/) → 2. --agents '{...}' CLI flag → 3. Project (.claude/agents/<name>.md) → 4. User (~/.claude/agents/<name>.md) → 5. Plugin (agents/<name>.md, invoked as plugin:agent).hooks, mcpServers, and permissionMode in frontmatter./subtask)Explore & Plan: (both run on the main conversation's model by default; a project/user subagent named Explore with model: haiku overrides the built-in for cheaper search) Read-only, skip root CLAUDE.md and git status, and are one-shot (return no agentId, cannot be resumed).general-purpose & Custom subagents: Load CLAUDE.md (unless omitClaudeMd: true), start in a fresh context window (no parent conversation history), and return an agentId that can be resumed via SendMessage./subtask): Unlike standard subagents, a fork inherits the parent's full conversation history, system prompt, tools, and model, and shares the parent's prompt cache (up to depth 2).--- name: security-auditor description: Audits diffs for OWASP issues. Use proactively after code changes. tools: Read, Grep, Glob # allowlist (omit to inherit all tools; no Agent = cannot spawn subagents) disallowedTools: Write, Edit, Agent # applied FIRST (Trap: Bash(git push *) strips ALL of Bash! Use permissions.deny) model: sonnet # Per-call Agent(model) > frontmatter > CLAUDE_CODE_SUBAGENT_MODEL > parent model permissionMode: plan # default | acceptEdits | auto | dontAsk | plan | bypassPermissions maxTurns: 15 # returns partial result on limit; resumable via SendMessage skills: [owasp-checklist] # preloads FULL skill body at startup (max 32; skips disable-model-invocation: true) mcpServers: [github, {playwright: {type: stdio, command: npx, args: ["-y", "@playwright/mcp"]}}] # string reuses parent; inline is subagent-only memory: project # user | project | local -> .claude/agent-memory/<name>/MEMORY.md (200 lines/25KB) + auto-enables Read/Write/Edit hooks: # subagent-scoped (Stop auto-converts to SubagentStop; requires folder trust even in -p) PreToolUse: [{matcher: "Bash", hooks: [{type: command, command: "${CLAUDE_PROJECT_DIR}/scripts/check.sh"}]}] isolation: worktree # temporary git worktree; auto-cleaned if no changes background: true # runs concurrently; cannot prompt interactively (AskUserQuestion fails) omitClaudeMd: true # skips user/project/local CLAUDE.md (managed policy CLAUDE.md still loads) ---
description vector & raw artifactsdescription (prompt-injection vector): A subagent's description is loaded into the main agent's system prompt so the parent knows when to delegate, whereas the markdown body is seen only by the subagent. Untrusted .claude/agents/*.md files can inject instructions into the parent via description before the subagent is ever invoked!Write/Edit) or include the verbatim artifact/diff in its final response.security-auditor) to review the diff cold.disallowedTools specifiers & permissionMode1. Putting a specifier in subagent disallowedTools — such as disallowedTools: Bash(git push *) — removes the entire Bash tool from the subagent. Keep Bash in tools and block specific commands via permissions.deny or a PreToolUse hook.
2. If the parent session runs in bypassPermissions, acceptEdits, auto, or plan, the parent mode overrides the subagent's permissionMode; a subagent setting permissionMode: bypassPermissions is ignored unless the parent session is already in bypassPermissions.
3. Under claude --agent, tools: Agent(worker, researcher) allowlists which subagents can be spawned; session-wide, block specific subagents via permissions.deny: ["Agent(Explore)"].
name and description (+ when_to_use, max 1,536 chars combined; total listing cap = 1% of context window via SLASH_COMMAND_TOOL_CHAR_BUDGET) load at startup. Frontmatter boundary trap: Trigger keywords placed inside the markdown body (below the closing ---) are invisible to Claude during skill selection; keep triggers in YAML frontmatter description.SKILL.md body (< 5,000 tokens on activation): Loaded only when invoked; after /compact, active skills re-attach up to 5,000 tokens/skill (25,000 total).scripts/, references/, assets/ (0 token cost for script code): Claude reads reference docs on demand and executes helper scripts via Bash (only script stdout/stderr enters context). Script failure trap: If a bundled script exits non-zero, Claude may fall back to standard tool reasoning; enforce command restrictions deterministically via permissions.deny or a PreToolUse hook.managed-settings)~/.claude/skills/<name>/SKILL.md).claude/skills/<name>/SKILL.md)<subdir>/.claude/skills/, auto-discovered on demand)<plugin>/skills/<name>/SKILL.md, namespaced /plugin:skill)claude.ai (~/.claude/skills/synced/, invoked as /anthropic-skills:<name>)~/.claude/skills/<different-name>/ so you can tweak it while teammates keep the project version. Reusing the same name silently shadows the project skill for you only (personal > project), which hides upstream fixes.| Frontmatter / Syntax | Effect & exam traps |
|---|---|
description + when_to_use | Trigger text shown in context (combined cap: 1,536 chars; put the primary trigger phrase first) |
disable-model-invocation: true | Manual-only (/name): description is omitted from context (zero token cost until invoked), Claude cannot invoke it via the Skill tool, and subagents cannot preload it. Use for side-effecting playbooks (/deploy, /release) |
user-invocable: false | Model-invocable background knowledge; hidden from the / menu (description stays in context) |
allowed-tools | Pre-approves listed tools while the skill runs (does not restrict available tools; clears on next user turn). To actually limit tools, use context: fork with an agent whose tools list is narrow, or permissions.deny |
context: fork + agent | Runs the skill in an isolated subagent with no conversation history (agent: Explore also skips CLAUDE.md, so SKILL.md must be self-contained) |
paths / hooks / model / effort / shell | paths: restricts auto-activation to matching file globs; hooks: registers skill-scoped lifecycle hooks |
$ARGUMENTS, $ARGUMENTS[N], $N | Substitutes arguments passed to /skill-name arg0 arg1 (appends ARGUMENTS: ... if omitted from body; also supports ${CLAUDE_SKILL_DIR} and ${CLAUDE_SESSION_ID}) |
!`command` preprocessing | Shell command inside !`...` runs before the skill prompt reaches Claude and inlines its stdout. Trap: it never shows a permission prompt — outside auto mode, an unapproved command or any non-zero exit code aborts the entire skill invocation |
| Surface | How a custom skill reaches it | Frontmatter & sync rules |
|---|---|---|
| Claude Code CLI / Desktop / IDE | Filesystem scopes + claude.ai downward sync (~/.claude/skills/synced/) | Full Claude Code frontmatter (allowed-tools, context: fork, hooks, argument-hint). One-way sync: claude.ai skills sync down to Claude Code, but local .claude/skills/ never sync up to claude.ai or the API |
| Agent SDK | .claude/skills/ when setting_sources includes "project"/"user", or preloaded per subagent via skills | Full body injected at startup when listed in subagent skills: [...] |
Claude API (/v1/skills) | Uploaded via Skills API (skill_id in container.skills + code_execution tool; workspace-wide, pin version in prod) | Runs in Anthropic's networkless container. Strict schema trap (verify: based on the Agent Skills spec; subject to change): API /v1/skills accepts only name, description, license, compatibility, metadata, allowed-tools — uploading a SKILL.md with argument-hint, disable-model-invocation, context, or hooks fails validation |
| Managed Agents | Attached to the Agent definition (skills: [...]) | Runs in the session's cloud or self-hosted environment |
| Scope | Location | Invoked as | Shared? |
|---|---|---|---|
| Project | .claude/commands/<name>.md (or skill .claude/skills/<name>/SKILL.md) | /name | Yes: committed, every teammate gets it on clone/pull |
| Personal | ~/.claude/commands/<name>.md (or ~/.claude/skills/<name>/SKILL.md) | /name | No: only you, all projects |
| Plugin | <plugin>/commands/ or <plugin>/skills/ | /plugin:name | Via marketplace |
Commands and skills are unified: a .claude/commands/review.md file and a .claude/skills/review/SKILL.md skill both create /review and accept the same frontmatter. Prefer a skill when you need supporting files (scripts, references).
| Frontmatter | Effect |
|---|---|
description | Shown in the / menu; also lets Claude invoke it automatically when relevant |
argument-hint | Autocomplete hint such as [focus-area] |
allowed-tools | Pre-approves listed tools while the command runs (it does not remove other tools) |
model | Model override for this command |
context: fork (+ agent) | Runs in a forked subagent with its own context; verbose output stays out of the main conversation, only the result returns |
disable-model-invocation: true | Only you can trigger it (use for side effects such as deploys) |
Body: $ARGUMENTS, $1, @file, !`cmd` | Argument substitution, file inclusion, and shell preprocessing (see Skills for the !`cmd` trap) |
--- description: Review the current branch against team standards argument-hint: [focus-area] allowed-tools: Read, Grep, Glob, Bash(git diff *) context: fork --- Review the changes on this branch (run `git diff main...HEAD`). Focus: $ARGUMENTS Apply the checklist in @docs/review-checklist.md and return findings as a table with file, line, severity and suggested fix.
Team workflow everyone should run the same way → project .claude/commands/ (or skill), committed. Personal shortcut → ~/.claude/commands/. Workflow that reads a lot of files or produces long output → add context: fork so the main session stays clean.
A command saved in ~/.claude/commands/ "doesn't exist" for teammates: personal scope is not in version control.
| Situation | Approach | Why |
|---|---|---|
| Diff fits in one sentence (typo, log line, rename) | Direct execution | Planning overhead exceeds its value |
| Clear bug with a stack trace in one file | Direct execution | Scope and fix are already known |
| Multi-file change, unfamiliar code, several valid approaches (framework migration, splitting a monolith, new integration) | Plan mode first | Explore safely and agree on an approach before edits; avoids costly rework |
| Large change whose approach is already decided | Plan mode to explore, then direct execution to implement | Combine: investigate read-only, then execute the agreed plan |
Ctrl+G to open the plan in your editor and change it before Claude proceeds. /model opusplan uses Opus while planning and Sonnet while executing.Explore subagent; only its summary returns.SPEC.md, so the interview noise doesn't fill the implementation context.Iterative refinement techniques:
| Technique | Use when |
|---|---|
| 2–3 concrete input → output examples | Prose descriptions get interpreted inconsistently (data transformations, output formats, naming) |
| Test-driven iteration: write tests first (including edge cases and performance limits), then iterate on failures | Behavior must be verifiable; failing tests tell Claude exactly what to fix |
| Give a pass/fail check (test command, build, linter, screenshot comparison) | Always: Claude can verify and self-correct instead of guessing |
| Specific feedback: actual vs expected output, exact error text | Replace "it doesn't work" with evidence |
| One message for interacting issues; sequential fixes for independent ones | Fixes that affect each other need to be considered together |
Plan mode is not a default for everything. Choosing plan mode for a one-line, well-understood fix (or direct execution for an architectural migration) is the wrong call.
| Tool | What it does | Pick it when | Trap |
|---|---|---|---|
| Grep | Searches file contents (ripgrep regex); output modes files_with_matches (default), content, count; filter with glob or type; respects .gitignore | Find callers of a function, an error string, all imports of a module | It doesn't find files by name; escape regex metacharacters |
| Glob | Finds files by name/path pattern (**/*.test.tsx); results sorted by modification time, capped at 100 | Locate files by extension, naming convention or directory | Doesn't search contents; doesn't respect .gitignore by default |
| Read | Reads a file (paged with offset/limit for large files) | Inspect a file before changing it | Required before Edit |
| Edit | Replaces an exact old_string with new_string | Targeted change in an existing file | Fails unless the file was read first, the match is exact and old_string is unique |
| Write | Creates or fully overwrites a file | New files, or a complete rewrite | Overwriting an existing file requires reading it first |
| Bash | Runs shell commands | Tests, builds, git, anything without a dedicated tool | Prefer Grep/Glob/Read over grep/find/cat for search and reading |
Edit failure → fallback:
→ Read the file again, then retry the edit.
old_string matches several places→ include more surrounding lines so it is unique, or set replace_all: true when every occurrence should change.
(repetitive generated code, many similar blocks) → Read the full file and Write the complete new content.
Incremental codebase exploration: start with Grep for entry points (function name, route, error message) → Read only the files that matter → follow imports with further Grep calls. To trace a function used through wrapper modules, first find every exported name that re-exports it, then Grep each name across the codebase. Don't read every file up front.
.claude-plugin/plugin.json (name, version, description); component folders (skills/, commands/, agents/, hooks/hooks.json, .mcp.json, .lsp.json, settings.json) live at the plugin root, never inside .claude-plugin/. (Plugin settings.json only supports the agent key.)${CLAUDE_PLUGIN_ROOT} vs ${CLAUDE_PLUGIN_DATA}: Installed plugins are copied into a read-only versioned cache (~/.claude/plugins/cache/...) that ${CLAUDE_PLUGIN_ROOT} points to — anything written inside ${CLAUDE_PLUGIN_ROOT} is wiped on update. Store persistent state, SQLite DBs, or node_modules in ${CLAUDE_PLUGIN_DATA} (~/.claude/plugins/data/<id>/), which survives updates.version in plugin.json overrides marketplace.json version and git SHAs. Pushing commits without bumping plugin.json version causes claude plugin update to skip fetching.userConfig with "sensitive": true (stored in OS keychain, exposed as ${CLAUDE_PLUGIN_OPTION_<KEY>})..claude-plugin/marketplace.json): Maps multiple named plugins inside a single repository so developers can install individual skills, subagents, or MCP servers via /plugin without loading the entire repo's context overhead.extraKnownMarketplaces: pre-registers org catalogs (github, git, url, npm, file).enabledPlugins: "plugin@marketplace": true force-enables a plugin in managed settings (users cannot disable it); false blocks it.strictKnownMarketplaces: allowlist of marketplace sources ([] blocks every marketplace, including the official directory); blockedMarketplaces blocklists specific sources.disableSideloadFlags: true: blocks --plugin-dir CLI sideloading (which bypasses marketplace allowlists).strictPluginOnlyCustomization: true (or selective strictCategories: ["hooks", "mcp"]): blocks non-managed local/user/project customizations so only managed settings and approved plugins can define hooks, skills, agents, or MCP servers.{
"strictKnownMarketplaces": [ { "source": "github", "repo": "acme-corp/plugins" } ],
"extraKnownMarketplaces": {
"acme-tools": { "source": { "source": "github", "repo": "acme-corp/plugins" } }
},
"enabledPlugins": { "secure-review@acme-tools": true },
"disableSideloadFlags": true,
"strictPluginOnlyCustomization": true
}
Claude Code acts as an MCP Host that spawns one dedicated MCP Client per configured server (strict 1:1 Client–Server relationship). Add servers via claude mcp add --transport http <name> <url> or claude mcp add --transport stdio <name> -- <cmd> [args...].
Scope (--scope) | Stored in | Precedence & sharing |
|---|---|---|
Session (--mcp-config) | CLI flag --mcp-config <file-or-json> | 0th (ephemeral session override); additive to other scopes unless paired with --strict-mcp-config (which ignores all user/project/local MCP servers) |
local (default for claude mcp add) | ~/.claude.json under projects["<path>"].mcpServers | 1st (highest persistent); private to you in this project |
project | .mcp.json at the repo root (committed to git) | 2nd; shared with the team (prompts for approval before first use; approval saved in gitignored .claude/settings.local.json via enabledMcpjsonServers) |
user | ~/.claude.json top-level mcpServers | 3rd (lowest); private to you across all projects |
| Plugin MCP | <plugin>/.mcp.json or inline in plugin.json | Lifecycle tied to plugin; tools named mcp__plugin_<plugin>_<server>__<tool> |
managed-mcp.json): Deploying managed-mcp.json at the OS system path gives IT exclusive control — users cannot add, modify, or use any MCP servers from .mcp.json, ~/.claude.json, or plugins. (To provision remote http/sse org servers without wiping user servers, use managedMcpServers in server-managed/MDM settings.)allowedMcpServers / deniedMcpServers): In managed-settings.json, entries match by {"serverName": "..."}, {"serverCommand": [...]} (exact stdio array), or {"serverUrl": "https://*.corp.com/*"}. Denylist always wins.serverUrl trap: Once any serverUrl entry appears in allowedMcpServers, remote HTTP/SSE servers must match a serverUrl pattern (serverName entries stop matching remote servers!).${VAR} expansion & Tool Search${VAR} and ${VAR:-default} expand in command, args, env, url, and headers. Unset variables without defaults send the literal ${VAR} string.ANTHROPIC_API_KEY and CLAUDE_CODE_* auth vars resolve to empty string in remote HTTP/SSE url/headers (causing 401). Use a custom env var name like ${SEARCH_MCP_TOKEN}.ENABLE_TOOL_SEARCH): By default (true/unset), all MCP tool schemas are deferred behind ToolSearch (set "alwaysLoad": true on a server in .mcp.json to preload its schemas). auto defers at 10% context (auto:<N> for custom %); false preloads all schemas up front.{
"mcpServers": {
"code-search": {
"type": "http",
"url": "${SEARCH_BASE_URL:-https://search.internal.example.com}/mcp",
"headers": { "Authorization": "Bearer ${SEARCH_MCP_TOKEN}" },
"alwaysLoad": true
}
}
}
claude -p)| Flag | Effect & CI nuances |
|---|---|
-p / --print | Non-interactive execution. Default mode behavior: plain claude -p (with no SDK canUseTool or --permission-prompt-tool) runs in default mode and immediately auto-denies any unapproved tool call (PermissionRequest hooks still get a chance to approve before denial unless mode is dontAsk). |
--bare | Clean-room CI mode: skips auto-discovery of CLAUDE.md, auto-memory, hooks, plugins, skills, and .mcp.json; ignores OAuth/keychain login (requires ANTHROPIC_API_KEY or cloud creds). Explicit --settings <file>, --mcp-config <file>, --plugin-dir, and --add-dir still work. |
--output-format text|json|stream-json | json returns a single summary object (result, session_id, total_cost_usd, subtype); stream-json emits newline-delimited JSON events (--include-partial-messages streams token deltas; --verbose includes full turn events). |
--json-schema '<schema>' | Enforces JSON Schema output after the agent finishes tool calls. Trap: the validated JSON object is returned in .structured_output (jq '.structured_output'), not .result! Check .subtype == "error_max_structured_output_retries" on schema failure. |
--allowedTools vs --tools | --allowedTools "Edit(/src/**),Bash(npm test *)" pre-approves matching calls (permissions.deny still wins); --tools "Read,Grep" restricts which built-in tools exist in context ("" disables all built-ins). |
--max-turns <N> / --max-budget-usd <N> / --fallback-model <m> | Caps agentic iterations (exits with subtype: "error_max_turns"), caps API spend, and auto-switches models when the primary model is overloaded (529). |
--system-prompt vs --append-system-prompt | --system-prompt replaces the default Claude Code prompt and drops the automatic git status/branch snapshot; --append-system-prompt (or --append-system-prompt-file) preserves defaults + git snapshot. |
--continue (-c), --resume <id> (-r), --fork-session, --no-session-persistence, --worktree (-w) | Continue/resume prior sessions, branch into a new session ID (--fork-session), skip writing transcripts to ~/.claude/projects/ (--no-session-persistence), or isolate the run in a git worktree (-w). |
/install-github-app)/install-github-app interactively in the CLI (requires authenticated gh CLI and repository admin permissions) to install the Claude GitHub App, configure repo secrets (CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY), and open a PR adding .github/workflows/claude.yml (@claude mentions on issues/PRs create isolated claude/issue/<N> branches — never pushing directly to main) and claude-code-review.yml (automated PR diff reviews).CLAUDE.local.md, .claude/settings.local.json, and ~/.claude/projects/.../memory/MEMORY.md do not exist in CI. Any rule or skill that PR review agents must follow must be committed to CLAUDE.md, .claude/rules/, or .claude/skills/./rewind (Esc+Esc)Esc twice (Esc+Esc) or run /rewind to pick a prior turn and restore: (1) Conversation and code, (2) Conversation only (keep disk edits, rewind context), or (3) Code only (revert disk edits, keep error lessons in context).Bash side-effect trap: Checkpointing and /rewind (as well as SDK rewind_files()) track only changes made through file-editing tools (Edit, Write, NotebookEdit). They never undo Bash side-effects (such as rm, mv, sed -i, git commit, package installs, or DB migrations) — use git or backups to recover from destructive shell commands.-p without --bare on untrusted PRsWhen running claude -p on an untrusted PR checkout without --bare, project permissions.allow rules and subagent frontmatter hooks are skipped, but repository .claude/settings.json hooks and .mcp.json stdio servers execute automatically without a workspace trust dialog! Always pass --bare (or --settings '{"disableAllHooks": true}' + --strict-mcp-config) when running -p against untrusted code.
# Re-review after new commits: give prior findings so only new or unresolved issues are reported
claude -p "Review the diff in pr.diff using the criteria in review-criteria.md. \
Prior findings are in prior-findings.json: report only issues that are new or still unresolved." \
--output-format json \
--json-schema "$(cat review-schema.json)" \
--allowedTools "Read,Grep,Glob" \
| jq '.structured_output.findings'
| Practice | Why |
|---|---|
-p for every pipeline run | Without it Claude Code starts an interactive session and the job waits for input |
--output-format json + --json-schema → read .structured_output | Machine-parseable findings (file, line, severity, issue, fix) your script can post as inline PR comments |
| Include prior findings on re-runs | Without them, each push re-posts the same comments; with them Claude reports only new or still-unaddressed issues |
| Independent review instance | The session that generated code keeps its own reasoning and is less likely to question it; review in a separate claude -p run that sees only the diff and criteria |
| Per-file passes + one integration pass for large PRs | Avoids attention dilution; see the Claude Agent SDK guide for decomposition patterns |
| Explicit criteria ("report bugs and security issues; skip style and naming") with severity definitions and examples | Vague "be conservative" prompts don't improve precision; false positives erode trust in every finding |
Commit project context (CLAUDE.md with testing standards, fixtures, review criteria) and pass existing test files | CI runners only see committed files; showing existing tests prevents duplicate test scenarios |
Blocking pre-merge check → synchronous claude -p. Overnight or weekly reports → Message Batches API | Batches cost less but can take up to 24 hours with no latency guarantee, so they suit only jobs nobody waits on |
(1) Re-running review on every push without the earlier findings → duplicate PR comments. (2) Asking the generating session to "review your own work" instead of a fresh instance. (3) --bare skips CLAUDE.md auto-discovery, so pass review criteria explicitly (prompt or --append-system-prompt-file) when you need clean-room mode on untrusted code (see Headless).
| Managed delivery source | Precedence & OS path / mechanism | Key constraints |
|---|---|---|
| 1. Server-managed / Gateway | claude.ai Admin Console (Teams/Enterprise) or self-hosted LLM Gateway | Fetched at startup + polled hourly (forceRemoteSettingsRefresh: true fails closed if fetch fails). Applies ONLY to claude.ai OAuth sessions — bypassed if a user sets ANTHROPIC_API_KEY, runs --bare, or uses Bedrock/Vertex/Foundry (lock auth via "forceLoginMethod": "claudeai" + "forceLoginOrgUUID" in file/MDM policy). Delivers one policy for the entire org. |
| 2. MDM / OS Registry | macOS plist (com.anthropic.claudecode) · Windows HKLM\SOFTWARE\Policies\Anthropic\ClaudeCode | Enforced by OS device management; supports per-group targeting via MDM device profiles. |
3. File-based (managed-settings.json + managed-settings.d/*.json) | macOS: /Library/Application Support/ClaudeCode/Linux / WSL: /etc/claude-code/Windows: C:\Program Files\ClaudeCode\ (legacy C:\ProgramData\ClaudeCode\ is ignored) | Drop-in files in managed-settings.d/*.json merge alphabetically on top of managed-settings.json (use 00-base.json, 10-sec.json). Verify active sources with /status. |
| 4. Windows User Registry | HKCU\SOFTWARE\Policies\Anthropic\ClaudeCode | Lowest managed source. |
managedSourcesBehavior trap: Defaults to "first-wins" — if Server-managed settings (Source 1) are non-empty, Claude Code completely ignores MDM (Source 2) and local managed-settings.json files (Source 3)! Set "managedSourcesBehavior": "merge" in the highest active managed source to combine them.allowManagedPermissionRulesOnly: true: ignores allow, ask, and deny rules from user, project, local, --settings, and --allowedTools.allowManagedHooksOnly: true: blocks user, project, and unforced plugin hooks (SDK callback hooks and hooks from plugins force-enabled in managed enabledPlugins still run).allowManagedMcpServersOnly: true: restricts allowedMcpServers to managed settings (deniedMcpServers still merges from all layers)./usage vs /cost) & rate protection/usage (/cost is for API key billing only); Admin Console analytics + OpenTelemetry (CLAUDE_CODE_ENABLE_TELEMETRY=1) + per-user/org spend limits./cost (or live via /statusline). Dedicated "Claude Code" workspace auto-created on first claude login; workspace spend limits & workspace rate limits isolate developer usage from production API traffic.claude_code.cost.usage, claude_code.token.usage); pair with an LLM Gateway (e.g. LiteLLM) for per-user virtual keys, spend caps, and rate limits.gcloud auth application-default login # Application Default Credentials export CLAUDE_CODE_USE_VERTEX=1 export CLOUD_ML_REGION=global # or a multi-region (us, eu) or region (us-east5) export ANTHROPIC_VERTEX_PROJECT_ID=my-gcp-project claude
| Step / setting | Detail |
|---|---|
| GCP prerequisites | Enable the Vertex AI API, request access to the Claude models in Model Garden, and grant roles/aiplatform.user (needs aiplatform.endpoints.predict). A dedicated project simplifies cost tracking and access control |
| Setup wizard | Run claude → 3rd-party platform → Google Vertex AI; it detects project/region, checks which models you can invoke and writes the env block of your user settings. Reopen with /setup-vertex (verify: requires a recent Claude Code version) |
| Regions | CLOUD_ML_REGION=global, a multi-region (us, eu) or a specific region. When using global, set VERTEX_REGION_CLAUDE_* to route models that have no global endpoint to a region |
| Optional | ANTHROPIC_VERTEX_BASE_URL (custom endpoint or gateway), DISABLE_PROMPT_CACHING=1, ENABLE_PROMPT_CACHING_1H=1 (1-hour cache TTL, higher write cost), CLAUDE_CODE_SKIP_VERTEX_AUTH=1 (gateway handles auth) |
| Behavior differences | /logout is unavailable (Google Cloud handles auth); MCP tool search is off by default (set ENABLE_TOOL_SEARCH=true on supported models); server-managed settings from claude.ai do not apply (use file or MDM managed settings) |
| Project ID override | GCLOUD_PROJECT, GOOGLE_CLOUD_PROJECT or the credentials file can override ANTHROPIC_VERTEX_PROJECT_ID |
| GitHub Actions | anthropics/claude-code-action@v1 with use_vertex: "true", authenticated through Workload Identity Federation (google-github-actions/auth) and a service account with the required Vertex AI role; no long-lived keys |
| Spend | Billed to the GCP project; track with Cloud Billing and Claude Code OpenTelemetry metrics, or an LLM gateway for per-user caps (see Team Rollout) |
"Could not load the default credentials" means ADC isn't set up: run gcloud auth application-default login (or point to a service account) after setting the project and region variables. Changing ANTHROPIC_API_KEY doesn't help, because Vertex calls use Google Cloud credentials.
| Symptom / scenario | Root cause → correct fix | Tempting wrong answer |
|---|---|---|
Edit(.claude/**) in allow or bypassPermissions still prompts on .claude/settings.json | Protected repo paths (.git, .claude, .vscode, .idea, .husky, .mcp.json) are checked before allow rules and even in bypassPermissions | Adding a broader Edit(**) allow rule |
Write(/etc/hosts) slips past deny: ["Write(/etc/**)"] | File-write rules must use Edit(...) (Write(...) never matches), and a single / anchors to CWD/~/.claude/ → use Edit(//etc/**) | Changing the glob to Write(/etc/*) |
PreToolUse hook on Read blocks Read(.env), but typing @.env in prompt leaks .env | @file mentions are inlined before the prompt without calling Read → add permissions.deny: ["Read(./.env)"] (or CLAUDE_CODE_DISABLE_FILE_MENTIONS=1) | Switching the hook from PreToolUse to UserPromptSubmit regex |
Guard hook exits 1 or times out and the destructive command still runs | Exit 1 (without JSON) and command/HTTP timeouts fail open → exit 2 with stderr reason and set "onFailure": "block" on the hook | Moving the guard to PostToolUse |
Subagent with disallowedTools: Bash(git push *) loses all Bash access | Any specifier in subagent disallowedTools removes the entire underlying tool → keep Bash in tools and block git push via permissions.deny or PreToolUse | Wrapping the specifier in quotes |
Adding CLAUDE.local.md causes Claude Code to stop following AGENTS.md | Default auto project-instructions setting switches to claude-md only when CLAUDE.local.md exists → set /config Project instructions to claude-md-and-agents-md | Copying AGENTS.md into .claudeignore |
Skill with !`gh pr view` aborts immediately in default mode | !`cmd` preprocessing never prompts; unapproved commands outside auto mode or non-zero exits abort the skill → pre-approve Bash(gh pr view *) in allow | Adding gh to skill allowed-tools (only applies after SKILL.md loads) |
Plugin SQLite DB inside ${CLAUDE_PLUGIN_ROOT} is wiped on update, or update never fetches new commits | ${CLAUDE_PLUGIN_ROOT} is wiped on update and plugin.json version wasn't bumped → store state in ${CLAUDE_PLUGIN_DATA} and bump version in plugin.json | Putting "version" only in marketplace.json |
Remote HTTP MCP server blocked despite {"serverName": "sentry"} in allowedMcpServers | Once any serverUrl entry exists in allowedMcpServers, remote servers must match a serverUrl pattern → add {"serverUrl": "https://*.sentry.io/*"} | Adding mcp__sentry__* to permissions.allow |
claude -p --json-schema returns empty .result in CI, or untrusted PR executes repo hooks | Schema output is in .structured_output (not .result), and -p without --bare auto-runs repo .claude/settings.json hooks → read .structured_output and pass --bare | Running claude -p in default mode without --bare |
Local managed-settings.json on macOS ignored after admin enables Server-managed settings | managedSourcesBehavior defaults to "first-wins" (Server > MDM > File) → set "managedSourcesBehavior": "merge" in the server-managed config | Moving the file to /etc/claude-code/ on macOS |
Running /compact after Claude hallucinates a non-existent API still repeats the hallucination | Context poisoning: /compact summarizes the current conversation, preserving poisoned assumptions in the summary → run /clear (or /rewind to a turn before the hallucination) | Running /compact a second time with a shorter prompt |
Skill with triggers placed only inside the markdown body below --- never auto-triggers | Only YAML frontmatter (description + when_to_use above the closing ---) loads at Level 1 startup → move trigger keywords into YAML frontmatter description | Adding user-invocable: true to frontmatter |
Running /rewind (Code only) after a failed migration script leaves deleted files and DB tables broken | Checkpointing tracks only Edit/Write/NotebookEdit changes and never undoes Bash side-effects → use git checkout / DB rollbacks for Bash mutations | Using Conversation and code restore mode in /rewind |
| Subagent generates a complete module internally but returns only a brief completion message without code or file paths | Subagents return only their final summary message to the parent → instruct the subagent to write files directly to disk (Write/Edit) or return the verbatim artifact/path | Increasing the parent agent's max_tokens |
Tests fail immediately inside a new --worktree (-w) session with Cannot find module | Git worktrees do not copy gitignored files (node_modules, .env, venv) → run dependency install / copy .env (or automate via a SessionStart hook) | Passing --add-dir node_modules |
A team commits the following and believes .env is completely blocked, warehouse MCP tools are auto-approved, and their CI JSON schema script is safe. Find four defects before revealing.
// .claude/settings.json { "permissions": { "allow": ["mcp__*", "Edit(.claude/**)"], "deny": ["Write(/.env*)"] }, "hooks": { "PreToolUse": [{ "matcher": "Read", "hooks": [{ "type": "command", "command": "/Users/alex/repo/.claude/hooks/block-env.sh" }] }] } } # .claude/hooks/block-env.sh f=$(jq -r '.tool_input.file_path') if [[ "$f" == *.env* ]]; then echo "blocked" >&2; exit 1; fi // .mcp.json { "mcpServers": { "warehouse": { "type": "http", "url": "https://wh.internal.example.com/mcp", "headers": { "Authorization": "Bearer ${ANTHROPIC_API_KEY}" } } } } # CI script claude -p "Extract release notes" --json-schema '{"type":"object","properties":{"tag":{"type":"string"}}}' --output-format json | jq -r '.result.tag'
Fix 1. Why are all three permission rules broken?
"mcp__*" in allow is unanchored and skipped (use "mcp__warehouse__query"); "Edit(.claude/**)" in allow never bypasses protected-path prompts on .claude/; and "Write(/.env*)" in deny never matches because file-write rules must use Edit(...) (and Read(./.env*) in deny is needed to block both Read/Edit/Write and @.env prompt mentions).
Fix 2. Why do .env reads still succeed and why does the hook break on other machines?
block-env.sh exits 1 (non-blocking fail-open) instead of 2, lacks "onFailure": "block", misses @.env prompt mentions, and hardcodes /Users/alex/... instead of ${CLAUDE_PROJECT_DIR}.
Fix 3. Why does the warehouse MCP server return 401 Unauthorized?
ANTHROPIC_API_KEY resolve to an empty string in remote MCP url/headers. Reference a custom variable name ("Bearer ${WAREHOUSE_MCP_TOKEN}").
Fix 4. Why does the CI pipeline print null for the schema output?
--json-schema places the validated JSON object in .structured_output.tag, not .result.tag (and untrusted PR runs should also pass --bare + --permission-mode dontAsk).
PreToolUse hooks → deny → ask → protected/critical-path checks → allow → permission mode → PermissionRequest hooks → prompt / canUseTool..git, .claude, .vscode, .idea, .husky, .mcp.json, shell profiles — checked before allow rules and still prompt in bypassPermissions.Edit(//path) vs Read(/path)Read and Edit take path globs (Edit covers Edit/Write/NotebookEdit); // = filesystem root, single / = <cwd> or ~/.claude/.MEMORY.md)~/.claude/projects/<project>/memory/MEMORY.md (first 200 lines / 25 KB loaded at startup; disable via CLAUDE_CODE_DISABLE_AUTO_MEMORY=1).PreToolUse decisionshookSpecificOutput.permissionDecision: deny > defer > ask > allow across parallel hooks, plus updatedInput and additionalContext."onFailure": "block"exit 1 without JSON), HTTP errors, and timeouts from fail-open warnings into hard blocks./subtask) vs Subagent${CLAUDE_PLUGIN_ROOT} vs DATAROOT is the read-only versioned cache wiped on update; DATA (~/.claude/plugins/data/<id>/) persists state across plugin updates.managed-mcp.json vs allowedMcpServersmanaged-mcp.json provisions an exclusive system-wide server set; allowedMcpServers / deniedMcpServers filter servers by serverName, serverCommand, or serverUrl.--bare (claude -p)CLAUDE.md, auto-memory, hooks, plugins, skills, .mcp.json, and OAuth/keychain.managedSourcesBehavior"first-wins" (Server/Gateway > MDM/HKLM > File + managed-settings.d/*.json > HKCU); set "merge" to combine.TodoWrite/TodoRead), (2) Hierarchical subagents, (3) Filesystem as external context engine, and (4) System prompt heuristics./clear, not /compact), Context confusion (tool bloat), and Context clash (conflicting rules)./output-style vs /statusline/output-style replaces the SWE system prompt (Default, Explanatory, Learning, custom); /statusline renders live JSON telemetry on stdin at 0 token cost./rewind (Esc+Esc) & /teleport/rewind restores code/conversation from checkpoints but never undoes Bash side-effects; /teleport pulls a cloud claude.ai/code session into your local terminal.A visual walkthrough of the Claude Code agentic loop, permission evaluation order, hooks, subagents, progressive-disclosure skills, plugins, MCP scopes, and Managed Agents.
Listen to the Claude Code & Agent SDK audio deep dive, or open the 2-column study guide PDF, the 20-slide Claude Code Architecture Blueprint deck, and the Agent SDK Architecture Map infographic in the Hub Viewer.
Q1. Why does deny: ["Write(/etc/**)"] fail to block Write("/etc/hosts"), and what is the exact rule that blocks it?
Read(...) and Edit(...) accept file-path patterns (Edit governs Edit, Write, and NotebookEdit; Write(...) never matches), and (2) a single leading slash / anchors to <cwd> or ~/.claude/, not the filesystem root. Use deny: ["Edit(//etc/**)"] with a double slash //.
Q2. Why does a PreToolUse hook matching Read fail to stop a user who types Summarize @.env, and how do you block both?
@file prompt mentions directly while building the prompt without invoking the Read tool, so PreToolUse never fires. Add Read(./.env) to permissions.deny (which blocks Read, Edit/Write, and @file mentions) or set CLAUDE_CODE_DISABLE_FILE_MENTIONS=1.
Q3. What happens when a subagent frontmatter includes disallowedTools: Bash(git push *)?
Bash tool, because any specifier in subagent disallowedTools strips the whole underlying tool. Keep Bash in tools and block git push via permissions.deny or a PreToolUse hook.
Q4. How does scope precedence differ between Subagents and Skills when a user-level and project-level definition share the same name?
managed > --agents > project > user > plugin). For Skills, user beats project (managed > user > project > nested > plugin > synced).
Q5. In a plugin, why shouldn't a hook write a SQLite database inside ${CLAUDE_PLUGIN_ROOT}?
${CLAUDE_PLUGIN_ROOT} points to the read-only versioned plugin cache, which is wiped and replaced whenever the plugin updates. Store persistent state in ${CLAUDE_PLUGIN_DATA} (~/.claude/plugins/data/<id>/).
Q6. What is the security risk of running claude -p on an untrusted PR branch without --bare, and where does --json-schema output appear?
--bare, claude -p executes repository .claude/settings.json hooks and .mcp.json stdio servers automatically without a workspace trust dialog. When using --json-schema with --output-format json, the validated payload is in .structured_output, not .result.
Q7. A team splits a 900-line CLAUDE.md into ten @docs/*.md imports to save context tokens. Does it work, and what would?
@ imports expand at launch, so the same tokens load every session; imports only organize content. To load topic guidance on demand, move it into .claude/rules/*.md files with paths: globs (loaded only when Claude reads a matching file) or into skills.
Q8. A CI review job re-posts the same comments on every push to a PR. What two changes fix it without lowering recall?
claude -p call and ask for only new or still-unresolved issues. (2) Emit findings with --output-format json --json-schema and read .structured_output, so the script can de-duplicate by file and line before posting inline comments.