Back to Hub
Certification Study Notes

Claude Code

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.

Author: Michaël Bettan
Track: Anthropic / Applied AI
Test Yourself: Claude Code Practice Quiz →
00

Overview, Agent Loop & Surfaces

Definition

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.

The agentic loop

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.

01

Gather context

CLAUDE.md, .claude/rules/, and auto-memory MEMORY.md load; Claude searches and reads files, or delegates noisy exploration to a subagent.

02

Permission check

PreToolUse hooks run first → deny → ask → protected/critical-path checks → allow → active permission mode → PermissionRequest hooks → prompt or SDK canUseTool.

03

Take action

Edits, shell commands, MCP tool calls, or skill invocations execute (optionally inside OS-level /sandbox isolation).

04

Verify

PostToolUse / FileChanged hooks format or audit; Claude runs tests and reads results before Stop hooks evaluate completion.

Surfaces: CLI, Worktrees, Cloud/Web & Teleport, Cowork, and Agent SDK

Claude Code (CLI, IDE & Worktrees)
Available in the terminal CLI, Desktop Code tab, VS Code / JetBrains extensions, Slack, and CI/CD. Local Worktrees (--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.
Cloud/Web (claude.ai/code) & Teleport
Runs in an Anthropic-managed cloud VM cloned from GitHub (survives laptop sleep; Mobile is 100% cloud-only). Prefix a CLI prompt with & <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).
Cowork, Dispatch & Agent SDK
Cowork runs desktop knowledge work in an app-hosted VM; Dispatch hands off tasks from mobile/web to desktop. The Claude Agent SDK embeds the same loop, tools, permissions, hooks, and subagents as a TypeScript or Python library in your own process (see the Claude Agent SDK guide).

Deep Agent 4-Pillar Architecture & the 4 Context Strategies

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

Mental model

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.

Scope of this guide

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.

01

1. Permission Modes & Allow / Ask / Deny Rules

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").

ModeRuns without askingStill prompts / blockedUse when
default (manual)File reads + built-in read-only shell commands (ls, pwd, git status/diff/log, rg, find without -exec/-delete) in working dirsEvery file edit, mutating shell command, and path outside working directoriesSensitive repos; reviewing every mutating action
acceptEditsEverything 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 rmIterating 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
autoActions approved by a background safety classifier; strips broad allow rules (Bash(*), Bash(python*), Bash(node*), Agent) on entryClassifier-flagged actions (curl-pipe-sh, prod deploys, force push, mass delete), protected paths, deny/ask rules; falls back to default if prerequisites failLong interactive tasks where prompt fatigue is the problem (disable org-wide via "disableAutoMode": "disable")
dontAskReads in working directories and explicitly pre-approved (allow) toolsAnything that would prompt is silently denied (PermissionRequest hooks and SDK canUseTool are skipped)Locked-down CI, scripts, and background jobs
bypassPermissionsAll tools and commands except the hard guardrails in the next columnExplicit deny rules, explicit ask rules, PreToolUse hook denials, protected repo paths, and critical-path rm/rmdirIsolated containers and disposable VMs only (disable org-wide via "disableBypassPermissionsMode": "disable")

Common trap: acceptEdits + protected paths

acceptEdits 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.

Rule evaluation & path anchors

  • Order: 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.
  • Bare vs scoped 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 prefix anchors: //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.
  • Cross-tool coupling: 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!

MCP rules & OS sandboxing (/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.
  • Never use (...) on mcp__ rules in settings files (skipped with a startup warning), and unanchored "*" or "mcp__*" in allow is skipped.
  • Permission rules vs OS sandboxing: Permission rules gate which tool calls Claude invokes; OS sandboxing (sandbox.enabled: true; Seatbelt on macOS, bubblewrap on Linux/WSL2) restricts child process filesystem and network access.
  • With 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.
JSON — .claude/settings.json (team-shared; requires workspace trust dialog for allow rules)
{
  "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"]
  }
}

Common trap: Bash prefix matching vs hard boundaries

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).

02

2. Settings & Memory Precedence

Precedence (high → low)WhereWho it affectsTypical content
1. ManagedServer-managed, MDM/OS policy, or managed-settings.json (+ managed-settings.d/*.json)Everyone the org deploys to; nothing below overrides a managed keySecurity policy, compliance, model/plugin/MCP locks
2. Command line--settings <file-or-json> and CLI flags (--permission-mode, --allowedTools)This session onlyOne-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.jsonEveryone who clones the repoTeam permissions, hooks, plugins, env
5. User~/.claude/settings.jsonYou, every projectPersonal defaults and rules

Merge rule

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 sourceLocation & loading behaviorShared with
Managed policy CLAUDE.mdOS 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 excludedWhole 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/editedTeam, via source control
Path-scoped .claude/rules/*.mdDiscovered recursively in .claude/rules/. Rules without paths: load at launch; rules with paths: ["src/api/**/*.ts"] load on demand when matching files are readTeam, 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)

Loading, imports, /init & monorepo rules

  • Target under 200 lines per CLAUDE.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.
  • Skip noisy ancestor files in monorepos via "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.
  • Audit instruction bloat/conflicts with /doctor prompt-audit and inspect token usage with /context. (Note: .claudeignore does not exist — use permissions.deny: ["Read(...)"].)

Subdir lazy-load trap, AGENTS.md & .claude/rules/

  • Subdirectory 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.

Context Rot Taxonomy & /clear vs /compact

  • Context poisoning: A hallucination, wrong file path, or failed assumption enters the conversation history and gets repeatedly referenced on later turns. Exam trap: /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).
  • Context confusion: Unrelated file dumps and verbose Bash outputs crowd the attention window; fix by delegating exploration to subagents and using /compact <focus> when switching phases.
  • Context clash: Contradictory instructions across CLAUDE.md files or multi-turn requirements cause unpredictable adherence; audit with /doctor prompt-audit.

Output Styles (/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.

Common trap

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.

03

3. CLAUDE.md Hierarchy, @ Imports & Path-Scoped Rules

Instruction is for…Put it inIn git?Trap
The whole team, every task./CLAUDE.md or ./.claude/CLAUDE.mdYesKeep it short (under ~200 lines); move topic detail into imports or rules
Only you, every project~/.claude/CLAUDE.mdNoTeammates never receive it: team conventions placed here "work on my machine" only
Only you, this repo./CLAUDE.local.mdNo (gitignored)Not shared across worktrees; import @~/.claude/<file>.md instead to reuse personal notes
One package or directory<subdir>/CLAUDE.mdYesLoads 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: globsYesA directory CLAUDE.md cannot target "every *.test.tsx anywhere"
The whole org, not excludableManaged policy CLAUDE.mdIT-deployedUsers cannot override or exclude it

@ imports (modular CLAUDE.md):

Markdown — CLAUDE.md with @imports
# 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

Path-scoped rules (.claude/rules/*.md):

Markdown — .claude/rules/api-and-tests.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.

Exam traps

(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.

04

4. Which Mechanism Carries Which Rule

Decision rule

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…UseWhy not the alternativesContext cost
Conventions for every task (test command, build steps)CLAUDE.mdA hook can't express style; a skill may not triggerEvery session; dilutes past ~200 lines
Guidance only for src/db/**.claude/rules/db.md with paths:In CLAUDE.md it wastes tokens on unrelated tasksOnly when matching files are touched
Block a tool, path, @file mention, or command prefix, alwayspermissions.deny (managed if org-wide)CLAUDE.md is advisory; PreToolUse skips @file mentionsZero (bare tool deny even removes tool schema)
Inspect/rewrite args or block dynamically before a tool runsPreToolUse hook (exit 2 or permissionDecision: "deny")Permission rules match globs/prefixes, not payload contentNear-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 writeNear-zero
Wide codebase search or independent parallel checkSubagent (.claude/agents/)Main context fills with throwaway file readsIsolated 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 sessionDescription 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 itReplaces default system prompt tokens
Live model, branch, or token/cost HUD in the terminalStatus line (/statusline in settings.json)Asking Claude or running hooks wastes tokens/turnsZero (runs out-of-band on stdin JSON)
Multi-step task & spec tracking across context compactionsTodoWrite / TodoRead + on-disk SPEC.mdConversational plans get lost or distorted during /compactMinimal structured state / on-demand file read
Ship skills, agents, hooks & MCP across many reposPlugin via a marketplaceCopy-pasted .claude/ folders drift across reposPer bundled component
Org policy users cannot overrideManaged settings (+ managed CLAUDE.md for guidance)Project/user files can be edited locallyZero

How mechanisms combine across scopes

  • 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.

Spec-Driven Development (SDD) & Two-Phase Model Switching

  • Phase 1 — Architect (Opus + 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).
  • Phase 2 — Builder (/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).
05

5. Hooks & Exit-Code Semantics

What a hook is, wildcard matchers & the 5 handler types

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).

Exit codeMeaningEffect on the event
0Success / allowEvent 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.
2Blocking errorBlocks 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.)
EventFires whenMatcherExit 2 / JSON block effect
PreToolUseBefore 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
PermissionRequestBefore a permission dialog shows (or before -p/background auto-deny outside dontAsk)Tool name✅ Denies via hookSpecificOutput.decision.behavior: "allow"|"deny"
PostToolUseImmediately 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
PostToolUseFailureAfter a tool execution throws or exits non-zero (not permission/schema rejections)Tool name❌ Feeds stderr / additionalContext to Claude alongside the tool error
UserPromptSubmitUser submits a prompt, before Claude processes it(none)✅ Blocks prompt processing and erases the prompt from context
Stop / SubagentStopMain 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)
SubagentStartSubagent spawns or resumesAgent name (Explore, custom)❌ Cannot block spawn; injects additionalContext into the subagent
SessionStart / SetupSession 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
PreCompactBefore context compaction (PostCompact fires after)manual|auto✅ Blocks compaction (exit 2 or decision: "block")
ConfigChangeSettings, managed policy, or skill file changes mid-sessionuser_settings|project_settings|local_settings|policy_settings|skills✅ Blocks config change from taking effect (except policy_settings, which cannot be blocked)
FileChanged / CwdChangedWatched file changes on disk (Edit, Bash, or external) / directory changesLiteral |-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 / SessionEndUser alert (permission_prompt, idle_prompt) / session close (1.5s timeout)Notification type / exit reason❌ Cannot block; alerting and cleanup only
JSON — fail-closed PreToolUse hook registration
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "if": "Bash(rm *)",
        "onFailure": "block",
        "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/no-rm.sh"
      }]
    }]
  }
}
Bash — 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

Structured JSON on 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).
  • Portable paths: Use ${CLAUDE_PROJECT_DIR} in repo hooks and ${CLAUDE_PLUGIN_ROOT} (code) / ${CLAUDE_PLUGIN_DATA} (persistent state) in plugin hooks.
  • Environment persistence ($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.
06

6. Subagents

5-level scope precedence & plugin restrictions

Specialized assistants with their own context window, system prompt, tool set, and optional model/permission mode.

  • Precedence (highest → lowest): 1. Managed (.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).
  • Contrast with Skills: For subagents, Project overrides User; for skills, User overrides Project.
  • Plugin security restriction: Plugin subagents ignore hooks, mcpServers, and permissionMode in frontmatter.

Built-in vs Custom vs Fork (/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.
  • Fork subagent (/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).
YAML — .claude/agents/security-auditor.md frontmatter
---
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)
---

Context contracts: description vector & raw artifacts

  • Dual role of description (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!
  • Summary-only return / missing raw artifact trap: A subagent's intermediate tool calls stay inside its isolated window; only its final text response returns to the parent. If the parent needs generated code or structured data, the subagent must either write it to disk (Write/Edit) or include the verbatim artifact/diff in its final response.

Wave scaling (3–5), Builder–Reviewer & when NOT to parallelize

  • Batch concurrency control: Orchestrate parallel subagent tasks in bounded batches of 3–5 concurrent subagents (each writing to distinct output paths) so the parent's context does not overflow when collecting results.
  • Builder–Reviewer separation: An agent reviewing code it just wrote in the same context suffers from confirmation bias; spawn a separate read-only subagent (security-auditor) to review the diff cold.
  • 5 anti-patterns where parallel agents/worktrees fail: (1) overlapping edits to the same files, (2) sequential data dependencies (Step B needs Step A's schema), (3) small single-file tasks where worktree/spawn overhead exceeds work, (4) ambiguous/unverified specs (multiplies rework by N), and (5) tight shared stateful resources (single local DB port).

Common traps: disallowedTools specifiers & permissionMode

1. 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)"].

07

7. Skills & Progressive Disclosure

3-Level Progressive Disclosure & Token Budgets

  • Level 1 — Metadata (~100 tokens/skill at startup): Only YAML frontmatter 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.
  • Level 2 — 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).
  • Level 3 — Bundled 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.

6 scopes & precedence (high → low)

  • 1. Enterprise (managed-settings)
  • 2. Personal (~/.claude/skills/<name>/SKILL.md)
  • 3. Project (.claude/skills/<name>/SKILL.md)
  • 4. Nested (<subdir>/.claude/skills/, auto-discovered on demand)
  • 5. Plugin (<plugin>/skills/<name>/SKILL.md, namespaced /plugin:skill)
  • 6. Synced claude.ai (~/.claude/skills/synced/, invoked as /anthropic-skills:<name>)
  • Personal variant of a team skill: copy it to ~/.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 / SyntaxEffect & exam traps
description + when_to_useTrigger text shown in context (combined cap: 1,536 chars; put the primary trigger phrase first)
disable-model-invocation: trueManual-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: falseModel-invocable background knowledge; hidden from the / menu (description stays in context)
allowed-toolsPre-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 + agentRuns 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 / shellpaths: restricts auto-activation to matching file globs; hooks: registers skill-scoped lifecycle hooks
$ARGUMENTS, $ARGUMENTS[N], $NSubstitutes arguments passed to /skill-name arg0 arg1 (appends ARGUMENTS: ... if omitted from body; also supports ${CLAUDE_SKILL_DIR} and ${CLAUDE_SESSION_ID})
!`command` preprocessingShell 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
SurfaceHow a custom skill reaches itFrontmatter & sync rules
Claude Code CLI / Desktop / IDEFilesystem 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 skillsFull 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 AgentsAttached to the Agent definition (skills: [...])Runs in the session's cloud or self-hosted environment
08

8. Custom Slash Commands

ScopeLocationInvoked asShared?
Project.claude/commands/<name>.md (or skill .claude/skills/<name>/SKILL.md)/nameYes: committed, every teammate gets it on clone/pull
Personal~/.claude/commands/<name>.md (or ~/.claude/skills/<name>/SKILL.md)/nameNo: only you, all projects
Plugin<plugin>/commands/ or <plugin>/skills//plugin:nameVia 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).

FrontmatterEffect
descriptionShown in the / menu; also lets Claude invoke it automatically when relevant
argument-hintAutocomplete hint such as [focus-area]
allowed-toolsPre-approves listed tools while the command runs (it does not remove other tools)
modelModel 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: trueOnly 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)
Markdown — .claude/commands/review-branch.md
---
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.

Decision rule

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.

Exam trap

A command saved in ~/.claude/commands/ "doesn't exist" for teammates: personal scope is not in version control.

09

9. Workflows: Plan Mode, Interview Pattern & Iterative Refinement

SituationApproachWhy
Diff fits in one sentence (typo, log line, rename)Direct executionPlanning overhead exceeds its value
Clear bug with a stack trace in one fileDirect executionScope and fix are already known
Multi-file change, unfamiliar code, several valid approaches (framework migration, splitting a monolith, new integration)Plan mode firstExplore safely and agree on an approach before edits; avoids costly rework
Large change whose approach is already decidedPlan mode to explore, then direct execution to implementCombine: investigate read-only, then execute the agreed plan

Iterative refinement techniques:

TechniqueUse when
2–3 concrete input → output examplesProse descriptions get interpreted inconsistently (data transformations, output formats, naming)
Test-driven iteration: write tests first (including edge cases and performance limits), then iterate on failuresBehavior 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 textReplace "it doesn't work" with evidence
One message for interacting issues; sequential fixes for independent onesFixes that affect each other need to be considered together

Exam trap

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.

10

10. Built-in Tools: Grep vs Glob, Read, Edit & Write

ToolWhat it doesPick it whenTrap
GrepSearches file contents (ripgrep regex); output modes files_with_matches (default), content, count; filter with glob or type; respects .gitignoreFind callers of a function, an error string, all imports of a moduleIt doesn't find files by name; escape regex metacharacters
GlobFinds files by name/path pattern (**/*.test.tsx); results sorted by modification time, capped at 100Locate files by extension, naming convention or directoryDoesn't search contents; doesn't respect .gitignore by default
ReadReads a file (paged with offset/limit for large files)Inspect a file before changing itRequired before Edit
EditReplaces an exact old_string with new_stringTargeted change in an existing fileFails unless the file was read first, the match is exact and old_string is unique
WriteCreates or fully overwrites a fileNew files, or a complete rewriteOverwriting an existing file requires reading it first
BashRuns shell commandsTests, builds, git, anything without a dedicated toolPrefer Grep/Glob/Read over grep/find/cat for search and reading

Edit failure → fallback:

01

Not read yet / file changed since read

→ Read the file again, then retry the edit.

02

old_string matches several places

→ include more surrounding lines so it is unique, or set replace_all: true when every occurrence should change.

03

Still no stable unique anchor

(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.

11

11. Plugins & Marketplaces

Plugin anatomy & cache vs data dirs

  • Manifest at .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 bump rule: 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.
  • Declare install-time secrets in userConfig with "sensitive": true (stored in OS keychain, exposed as ${CLAUDE_PLUGIN_OPTION_<KEY>}).

Marketplace governance & granular install

  • Granular catalog (.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.
JSON — managed-settings.json (curated plugin rollout)
{
  "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
}
12

12. MCP in Claude Code

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 inPrecedence & 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>"].mcpServers1st (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 mcpServers3rd (lowest); private to you across all projects
Plugin MCP<plugin>/.mcp.json or inline in plugin.jsonLifecycle tied to plugin; tools named mcp__plugin_<plugin>_<server>__<tool>

Managed MCP: Provisioning vs Filtering

  • 1. Exclusive file provisioning (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.)
  • 2. Policy filtering (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.
  • Credential variable strip: 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}.
  • MCP Tool Search (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.
JSON — .mcp.json (project scope, committed)
{
  "mcpServers": {
    "code-search": {
      "type": "http",
      "url": "${SEARCH_BASE_URL:-https://search.internal.example.com}/mcp",
      "headers": { "Authorization": "Bearer ${SEARCH_MCP_TOKEN}" },
      "alwaysLoad": true
    }
  }
}
13

13. Headless, CI/CD, Checkpointing & GitHub Actions (claude -p)

FlagEffect & CI nuances
-p / --printNon-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).
--bareClean-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-jsonjson 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).

GitHub Actions Integration (/install-github-app)

  • Setup & prerequisites: Run /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).
  • Ephemeral CI runner trap: GitHub Actions runners are fresh checkouts — gitignored 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/.

Checkpointing & /rewind (Esc+Esc)

  • 3 restore modes (30-day retention): Claude Code automatically snapshots tracked files before every user prompt and file-editing tool call. Press 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).
  • Critical 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.

Critical CI security trap: -p without --bare on untrusted PRs

When 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.

14

14. Code Review & Test Generation in CI/CD

Bash — CI re-review with prior findings
# 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'
PracticeWhy
-p for every pipeline runWithout it Claude Code starts an interactive session and the job waits for input
--output-format json + --json-schema → read .structured_outputMachine-parseable findings (file, line, severity, issue, fix) your script can post as inline PR comments
Include prior findings on re-runsWithout them, each push re-posts the same comments; with them Claude reports only new or still-unaddressed issues
Independent review instanceThe 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 PRsAvoids 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 examplesVague "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 filesCI 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 APIBatches cost less but can take up to 24 hours with no latency guarantee, so they suit only jobs nobody waits on

Exam traps

(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).

15

15. Team Rollout, Distribution & Spend Controls

Managed delivery sourcePrecedence & OS path / mechanismKey constraints
1. Server-managed / Gatewayclaude.ai Admin Console (Teams/Enterprise) or self-hosted LLM GatewayFetched 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 RegistrymacOS plist (com.anthropic.claudecode) · Windows HKLM\SOFTWARE\Policies\Anthropic\ClaudeCodeEnforced 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 RegistryHKCU\SOFTWARE\Policies\Anthropic\ClaudeCodeLowest managed source.

Intra-managed precedence & lockdown keys

  • 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).

Spend visibility (/usage vs /cost) & rate protection

  • Claude Pro / Max / Teams / Enterprise (Subscription): Monitor rolling plan quota via /usage (/cost is for API key billing only); Admin Console analytics + OpenTelemetry (CLAUDE_CODE_ENABLE_TELEMETRY=1) + per-user/org spend limits.
  • Claude Console (API keys): Monitor per-session token/dollar spend via /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.
  • Bedrock / Vertex / Foundry: Cloud billing + OTel metrics (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.
16

16. Claude Code on Google Cloud (Vertex AI)

Bash — Vertex AI environment
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 / settingDetail
GCP prerequisitesEnable 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 wizardRun 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)
RegionsCLOUD_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
OptionalANTHROPIC_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 overrideGCLOUD_PROJECT, GOOGLE_CLOUD_PROJECT or the credentials file can override ANTHROPIC_VERTEX_PROJECT_ID
GitHub Actionsanthropics/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
SpendBilled to the GCP project; track with Cloud Billing and Claude Code OpenTelemetry metrics, or an LLM gateway for per-user caps (see Team Rollout)

Exam trap

"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.

17

17. Failure Modes & Find the Defect

Symptom / scenarioRoot cause → correct fixTempting wrong answer
Edit(.claude/**) in allow or bypassPermissions still prompts on .claude/settings.jsonProtected repo paths (.git, .claude, .vscode, .idea, .husky, .mcp.json) are checked before allow rules and even in bypassPermissionsAdding 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 runsExit 1 (without JSON) and command/HTTP timeouts fail open → exit 2 with stderr reason and set "onFailure": "block" on the hookMoving the guard to PostToolUse
Subagent with disallowedTools: Bash(git push *) loses all Bash accessAny specifier in subagent disallowedTools removes the entire underlying tool → keep Bash in tools and block git push via permissions.deny or PreToolUseWrapping the specifier in quotes
Adding CLAUDE.local.md causes Claude Code to stop following AGENTS.mdDefault auto project-instructions setting switches to claude-md only when CLAUDE.local.md exists → set /config Project instructions to claude-md-and-agents-mdCopying 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 allowAdding 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.jsonPutting "version" only in marketplace.json
Remote HTTP MCP server blocked despite {"serverName": "sentry"} in allowedMcpServersOnce 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 hooksSchema output is in .structured_output (not .result), and -p without --bare auto-runs repo .claude/settings.json hooks → read .structured_output and pass --bareRunning claude -p in default mode without --bare
Local managed-settings.json on macOS ignored after admin enables Server-managed settingsmanagedSourcesBehavior defaults to "first-wins" (Server > MDM > File) → set "managedSourcesBehavior": "merge" in the server-managed configMoving the file to /etc/claude-code/ on macOS
Running /compact after Claude hallucinates a non-existent API still repeats the hallucinationContext 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-triggersOnly YAML frontmatter (description + when_to_use above the closing ---) loads at Level 1 startup → move trigger keywords into YAML frontmatter descriptionAdding user-invocable: true to frontmatter
Running /rewind (Code only) after a failed migration script leaves deleted files and DB tables brokenCheckpointing tracks only Edit/Write/NotebookEdit changes and never undoes Bash side-effects → use git checkout / DB rollbacks for Bash mutationsUsing Conversation and code restore mode in /rewind
Subagent generates a complete module internally but returns only a brief completion message without code or file pathsSubagents 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/pathIncreasing the parent agent's max_tokens
Tests fail immediately inside a new --worktree (-w) session with Cannot find moduleGit 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

Find the defect: a team's "secure" setup

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 + .claude/hooks/block-env.sh + .mcp.json + CI script
// .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'

Reveal the Four Fixes (Click to Reveal)

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?

Built-in credential variables like 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).
18

Glossary, Practice Quiz & Self-Assessment

Permission evaluation order
PreToolUse hooks → deny → ask → protected/critical-path checks → allow → permission mode → PermissionRequest hooks → prompt / canUseTool.
Protected paths
.git, .claude, .vscode, .idea, .husky, .mcp.json, shell profiles — checked before allow rules and still prompt in bypassPermissions.
Edit(//path) vs Read(/path)
Only Read and Edit take path globs (Edit covers Edit/Write/NotebookEdit); // = filesystem root, single / = <cwd> or ~/.claude/.
Auto memory (MEMORY.md)
Claude-written notes at ~/.claude/projects/<project>/memory/MEMORY.md (first 200 lines / 25 KB loaded at startup; disable via CLAUDE_CODE_DISABLE_AUTO_MEMORY=1).
PreToolUse decisions
hookSpecificOutput.permissionDecision: deny > defer > ask > allow across parallel hooks, plus updatedInput and additionalContext.
"onFailure": "block"
Hook setting that converts non-zero exits (like exit 1 without JSON), HTTP errors, and timeouts from fail-open warnings into hard blocks.
Fork (/subtask) vs Subagent
Standard subagents get a fresh context window; a fork inherits parent conversation history, tools, model, and prompt cache (up to depth 2).
${CLAUDE_PLUGIN_ROOT} vs DATA
ROOT is the read-only versioned cache wiped on update; DATA (~/.claude/plugins/data/<id>/) persists state across plugin updates.
managed-mcp.json vs allowedMcpServers
managed-mcp.json provisions an exclusive system-wide server set; allowedMcpServers / deniedMcpServers filter servers by serverName, serverCommand, or serverUrl.
--bare (claude -p)
Clean-room headless flag that skips CLAUDE.md, auto-memory, hooks, plugins, skills, .mcp.json, and OAuth/keychain.
managedSourcesBehavior
Intra-managed rule: defaults to "first-wins" (Server/Gateway > MDM/HKLM > File + managed-settings.d/*.json > HKCU); set "merge" to combine.
Deep Agent 4 Pillars
(1) Explicit planning (TodoWrite/TodoRead), (2) Hierarchical subagents, (3) Filesystem as external context engine, and (4) System prompt heuristics.
Context Rot Taxonomy
Context poisoning (hallucinations locked into history; requires /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.

Claude Code & Agent SDK Architecture Blueprint Video

A visual walkthrough of the Claude Code agentic loop, permission evaluation order, hooks, subagents, progressive-disclosure skills, plugins, MCP scopes, and Managed Agents.

Audio Deep Dive & Study Downloads

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.

How to Safely Harness Claude Code & Agent SDK (Podcast)
Open Study Notes, Slide Deck & Infographic in Hub Viewer Launch Interactive Claude Code Practice Quiz →

Self-Assessment Questions (Click to Reveal)

Q1. Why does deny: ["Write(/etc/**)"] fail to block Write("/etc/hosts"), and what is the exact rule that blocks it?

Two reasons: (1) only 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?

Claude Code inlines @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 *)?

The subagent loses the entire 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?

For Subagents, project beats user (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?

Without --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?

No. @ 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?

(1) Pass the previous run's findings into the next 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.