Domain 2 โ Facts
Claude Code ยท the things to remember, in plain words
Each item is one rule that decides exam questions. Read the short headline, say the rule to yourself, then read the line to check. Open why if the reason is not clear, and follow the question link to see the rule in action. Collected from the lesson, the practice questions and two full third-party mock exams. They help you remember; they do not guarantee a score.
Back to Domain 2 lesson ยท Domain 2 โ Questions ยท Domain Q&A
Memory files & rules
- Which CLAUDE.md wins? None of them wins. Claude Code adds them all together into context:
~/.claude/CLAUDE.md, the projectCLAUDE.md, andCLAUDE.local.md. If two rules disagree, the model may follow either one. "The more specific file wins" is the trap. โ wrong choice to avoid: keeping the rule in the project CLAUDE.md because "the more specific file wins"why
Claude Code reads every memory file that applies to your folder and appends them, broadest first. The docs call this a load order, not a priority order: nothing deletes the earlier text, so both rules stay in context. That's why a project rule can't reliably beat a developer's personal
~/.claude/CLAUDE.md. If a rule must hold every time, put it in settings permissions or a hook โ the client enforces those, whatever the model decides. - Rule must always hold? If a rule must hold on every run, don't leave it as text โ move it to
settings.jsonpermissions or a hook. Moving the text toCLAUDE.local.mdonly changes the load order, and that file is gitignored, so teammates never get it. โ wrong choice to avoid: moving the rule to CLAUDE.local.md so it loads last and "wins"why
CLAUDE.mdtext is guidance for the model, with no guarantee it is followed. Permissions and hooks are run by the Claude Code client itself: adenyrule stops the call, and aPreToolUseorPostToolUsehook runs as a shell command no matter what the model wrote. Putting the sentence inCLAUDE.local.mdjust makes it the last file added; the conflict is still in context. And because that file is gitignored, a team rule there never reaches teammates. - When does a nested file load? Files above your folder load when the session starts. A nested
services/billing/CLAUDE.mdloads only when Claude starts working in that folder. So if the rules belong to one folder tree, put them in a nested file there.why
At session start, Claude Code reads the memory files above the working directory. A
CLAUDE.mdinsideservices/billing/stays unread until Claude touches files under that path; then it is pulled in and adds to the parent files โ it does not replace them. That keeps billing rules out of unrelated sessions with no extra setup. If the rules belong to a file type spread across many folders, use apaths:rule instead. - Rules for one file type? Put them in
.claude/rules/*.mdwithpaths: ["**/*.test.ts"]frontmatter. Then the rule loads only when Claude edits a matching file, anywhere in the repo. A rules file withoutpaths:just loads at session start. โ wrong choice to avoid: a section heading in the root CLAUDE.md, or copying CLAUDE.md into every folderwhy
Rules files let you split one big
CLAUDE.mdinto topic files. Add apaths:list of glob patterns (file-name patterns) and the file becomes conditional: it enters context only when Claude edits a matching file, wherever it lives. That saves tokens and stops, say, Terraform rules showing up in React work. A section heading in the root file, a copy of the file in every folder, or a slash command you must type yourself โ none of these switch on automatically by file pattern. - File type or folder? Ask: is the rule about a file type or a folder? A file type spread across folders โ a
paths:rule. One folder tree โ a nestedCLAUDE.md. A heading in the root file always loads, and@importloads everything too.why
Pick the tool by the shape of the rule. Rules for a file type that shows up in many folders (test files,
.tffiles) need a.claude/rules/file withpaths:globs, because it matches by pattern. Rules for one folder tree need a nestedCLAUDE.md, because it loads by location. A heading in the root file loads with everything else, and an@importline pulls its file in at session start โ neither one is conditional. - Team rule or personal? Team rules go in the committed project
CLAUDE.md. Your~/.claude/CLAUDE.mdis not in git, so teammates and CI never see it. If the team needs a rule, it can't live only in your home folder. Practice Q&A ยท Q10 ยท โ wrong choice to avoid: each engineer's settings.local.json, or a missing "activation header" on the user-level filewhy
The user-level file sits in your home directory and applies to every project you open, but it is never part of the repo. When a teammate pulls or CI checks out the code, only committed files travel: the project
CLAUDE.md,.claude/rules/, settings and skills. A standard written only in~/.claude/CLAUDE.mdlooks like "no standard" to everyone else. Keep personal preferences in the user file; put team standards in the project file and review them like code. - What did this session load? Run
/memory. It lists the CLAUDE.md and rules files this session actually loaded. Asking Claude to repeat the rule, deleting files, or running/compactproves nothing. โ wrong choice to avoid: asking Claude to repeat the rules back, or running /compact to "reload" configwhy
/memorygives a sure answer: it prints the user, project and folder-level memory files in this session, so a stray user file or a missing project file shows up right away. Asking the model to repeat the rule proves nothing โ it can answer from training data or partial context. Deleting~/.claude/CLAUDE.mdis a destructive guess./compactonly summarizes the conversation; it does not reload or report config. - Quick memory shortcuts Four shortcuts: start a message with
#to add a memory line;/memoryto edit the files;/initto create a starterCLAUDE.mdfor a repo;@docs/architecture.mdto import a file. Imports still cost context.why
A message that starts with
#adds that line to a memory file without opening an editor./memoryopens the loaded files for editing./initscans the repo and writes a firstCLAUDE.mdwith its build and test commands. An@pathline inside a memory file pulls in another file when it loads. It keeps the root file short, but the imported text still uses context every session โ tidier, not cheaper. - Rules for one client only? Put them in a subdirectory
CLAUDE.md(for example inclients/acme/) and run that job from there. It adds to the parent file without changing it.--append-configis not a real flag. โ wrong choice to avoid: passing the client rules in the user message at the start of each runwhy
Memory files stack by folder: a
CLAUDE.mdinclients/acme/is added on top of the root file, so the stricter rules apply only when the job runs from there, and every other run keeps the defaults.CLAUDE.mdhas no if-logic, so an "if the brief contains CLIENT_ID" block is just text.--append-configdoesn't exist; the real--append-system-prompt-fileflag adds to the system prompt, which is a different mechanism. Putting the rules in the user message works once, but it is not config. - Wrong kind of tests? If Claude writes mocked unit tests when you wanted integration tests, your rule was too vague. Spell out the criteria in
CLAUDE.md: real database, contract assertions. A rule or hook that bans all mocks goes too far. โ wrong choice to avoid: a .claude/rules/ file on test files that tells Claude never to use mockswhy
The usual cause is that the instruction never said what an integration test must include. Concrete criteria โ a real database connection, API contract checks, migration rollback, an example fixture โ give the model a clear target and fix it at the source. A
paths:rule orPostToolUsehook that rejects every mock also kills good unit tests. A review subagent that rewrites tests afterwards is less reliable and more complex than just saying what you need.
Settings, permissions & precedence
- Which setting wins? Strongest first: managed policy โ CLI flags โ
.claude/settings.local.jsonโ.claude/settings.jsonโ~/.claude/settings.json. Remember: a CLI flag never beats managed policy. Practice Q&A ยท Q45 ยท โ wrong choice to avoid: CLI flags above managed policy, or settings.local.json above CLI flagswhy
For single-value settings, these layers have a strict order, and the exam tests the whole chain. Managed policy comes from your organization, and nothing below can override it. A command-line value beats every file. Your personal, gitignored local project file beats the shared project file. Your home settings are weakest. This order is for settings only โ
CLAUDE.mdtext is added together, not ranked. - Three ways config merges Config merges three different ways. Single values like
modeloverride (the strongest layer wins).permissions.allow/denylists add up across all layers. Memory files are just joined together. An answer that calls all three "the hierarchy" is wrong.why
So it depends on the key. A single value such as
modelfollows the precedence chain. Permission lists (allow,ask,deny) are combined from every layer, which is why a deny anywhere still applies. Memory files are pasted into context with no ranking at all. An answer that describes one hierarchy for everything is wrong about at least one of the three. - Does deny beat allow? Yes, always. A matching
denybeats anyallowfrom any layer, even--allowedTools. And removing an allow entry is not a deny โ it only means Claude Code asks you instead of running the call automatically. โ wrong choice to avoid: a PreToolUse hook that waits for human approval, or a new request tool that needs server changeswhy
Claude Code checks deny rules first: if any layer denies a tool pattern, the call is blocked, however many allows match. It is not symmetric โ dropping a tool from the allow list only removes pre-approval, so you get a prompt, not a block. To really take a tool away, use
deny,disallowedToolsor--tools. For a pipeline that needs human approval, hard-block the tool and tell the model to write a request for the human queue: aPostToolUsehook fires after the damage, and aPreToolUsehook can't pause and wait for a person. - How to read a rule A rule is
Tool(pattern).Bash(npm run test:*)means theBashtool, only for commands startingnpm run test.deny: ["Read(.env*)"]makes secret files unreadable.mcp__docs__load_documentnames one MCP tool.why
Bash(npm run test:*)pre-approves only shell commands that start withnpm run test;*is the wildcard, and other Bash commands are not covered.Read(.env*)in thedenylist stops the model from reading any file whose name starts with.env, so secrets never enter context. MCP tools use the same system with the namemcp__<server>__<tool>, so you can allow one tool on thedocsserver and deny another one from the same server. - Just my own setting? If a setting is only for you in this project, like
"model": "claude-haiku-4-5", put it in.claude/settings.local.jsonโ it's gitignored..claude/settings.jsonis committed, so every teammate gets it. Practice Q&A ยท Q41 ยท โ wrong choice to avoid: putting the model key in the committed .claude/settings.json, or as text in CLAUDE.mdwhy
The two project files differ only in who sees them.
.claude/settings.jsonis committed, so anything there is a team decision that every clone inherits..claude/settings.local.jsonis your personal override for this checkout; it beats the shared file and must stay out of git. Writing "use Haiku" as text inCLAUDE.mddoes not set the model. Managed policy is for company-wide controls, not one person's preference. - What managed settings do Think of
managed-settings.json(in/Library/Application Support/ClaudeCode/or/etc/claude-code/) as a clamp: nothing below can loosen what it sets. But it only controls the keys it actually sets; lower layers decide the rest.why
IT puts managed settings in a system path normal users can't edit โ
/Library/Application Support/ClaudeCode/managed-settings.jsonon macOS,/etc/claude-code/on Linux. Any key it sets โ adenyon reading.envfiles,disableBypassPermissionsMode, a forced login method โ sits at the top, and because permission lists add up, no lower file can cancel them. It is a clamp, not a full config: keys it doesn't mention are left to the layers below. - Skip all permissions?
--dangerously-skip-permissionsis only for isolated containers or CI. On the exam, the right answer is scoped allowlists. A company can switch the flag off everywhere with manageddisableBypassPermissionsMode: "disable". Practice Q&A ยท Q12 ยท โ wrong choice to avoid: --dangerously-skip-permissions, or just trusting the model to follow policywhy
The flag removes the permission prompt completely. That's handy in a throwaway container, but it deletes the exact check a security question wants you to keep. The expected design is narrow
allowrules likeBash(npm run test:*), credentials that can't publish, and a real network sandbox โ controls that still work when the model makes a mistake or reads malicious input. To ban the flag org-wide, setdisableBypassPermissionsMode: "disable"in managed settings; no lower layer can loosen it. - Is ~/.claude.json a setting? No.
~/.claude.jsonis client state: trust decisions plus user- and local-scope MCP servers, stored by project path. Shared project servers go in.mcp.json. Same server name in two places โ local beats project, project beats user.why
Claude Code keeps
~/.claude.jsonfor itself: folder trust, global preferences and MCP server definitions. It is not a fifthsettings.jsonlayer, and there is no project-levelclaude.json. MCP scope means who a server is for, not where the file lives: user-scope and local-scope (per project path) servers both live in this home file, while project-scope servers your team shares live in.mcp.jsonat the repo root. - What allowedTools really means In Claude Code,
allowedToolsmeans "run without asking" โ it is not the full list of tools. To take a tool away, use--tools,disallowedToolsordeny. And prefer an allowlist over adisabledToolsblocklist. โ wrong choice to avoid: a disabledTools blocklist that names only the two destructive toolswhy
Tools you didn't list still exist; they may just prompt.
--toolspicks which built-in tools exist, anddisallowedTools/denyblock a pattern. But careful: in the exam's pipeline scenarios,allowedToolson a subagent or SDK query is treated as the hard set of tools the agent can reach. That holds because the run is unattended: nobody can approve a tool that is not pre-approved, so its call is denied. There, an allowlist beats adisabledToolsblocklist, because any tool added to the server later is unreachable by default. ACLAUDE.mdsentence is only soft advice, and aPreToolUsehook only reacts after the model already chose the tool.
Skills ยท commands ยท subagents
- Where do skills live? Location decides who gets it. A skill in
.claude/skills/<name>/SKILL.mdis committed, so every clone gets/extract-service;~/.claude/skills/is just you. Old-style commands:.claude/commands/<name>.md. Subagents:.claude/agents/<name>.md. โ wrong choice to avoid: an inline procedure in the root CLAUDE.md, or ~/.claude/skills/ shared through the team wikiwhy
Anything under the repo's
.claude/folder is in git, so the skill shows up in every clone and updates arrive withgit pull.~/.claude/skills/is per user; sharing it through a wiki makes machines drift apart.CLAUDE.mdis for always-loaded standards, not an on-demand multi-step procedure, and an MCP server gives tools and data, not prompt workflows. Subagents use the same layout in.claude/agents/. - Who runs a skill? "Model-invoked" means the model picks the skill by matching its
description. Then Claude Code loads it and runs the tools that permissions allow. The model never runs scripts itself. Only hooks fire with nobody choosing. Practice Q&A ยท Q42 ยท Practice Q&A ยท Q47 ยท โ wrong choice to avoid: "the model chose it and the model ran it", or "the harness fired it on an event"why
There are three actors: you, the model, and the harness (Claude Code, the program around the model). The model reads the skill's
description, decides it fits, and asks for it; the harness loads the skill and runs any tool calls the permission rules allow. Typing/nameis a second way in. A hook is different: the harness fires it on a lifecycle event, with nobody choosing. Picking a skill is not permission โ tool rules still decide what runs. - Who can trigger a skill? Two switches.
disable-model-invocation: trueโ only you, by typing/name.user-invocable: falseโ only the model can pick it. Leave both out โ either way works. Neither one grants tool permissions.why
Each flag turns off one of the two routes.
disable-model-invocation: truestops Claude from choosing the skill by itself โ right for a deploy skill you never want fired by a casual "go live" sentence.user-invocable: falsehides the slash command and leaves the choice to the model. Neither flag touches permissions: the skill still needs tool rules and credentials that allow what it does. - What context: fork does
context: forkruns the skill in a separate subagent context, so only a summary comes back.agent: general-purposeon its own line picks who runs it. Leave outcontextand it runs inline. It is not a git worktree, and not parallel. Practice Q&A ยท Q48 ยท โ wrong choice to avoid: thinking fork makes a git worktree, or runs in parallel and merges backwhy
By default a skill loads inline, so a long analysis skill fills your main conversation and Claude can lose the original task.
context: forksends the body to a subagent with its own context window; the namedagent:runs it there and only a summary returns. That is context isolation, nothing more: same working files (no git worktree), not running at the same time as the main session, and no copy of the whole conversation. Splitting the skill or using a smaller model still dumps everything into the main context. - Does allowed-tools restrict? No.
allowed-tools: Bash, Readin SKILL.md only pre-approves those tools while the skill runs. It removes nothing, anddeny/askrules still win.argument-hintjust shows a hint when you type the command.why
allowed-toolslists tools the skill can use without asking each time. It is a convenience, not a limit: unlisted tools are still available, and adenyoraskrule from any settings layer still beats it. So real read-only behavior comes from permission rules or a subagent's tool list, not from this field.argument-hintis even lighter โ it shows a hint in command completion and doesn't check or require anything. - Same prompt, one input? If your team types the same 20-line prompt and only one value changes, make it a slash command with
$ARGUMENTS(.claude/commands/release.md, or a skill with/name). Not a hook, not a subagent. Practice Q&A ยท Q11 ยท Practice Q&A ยท Q43 ยท โ wrong choice to avoid: a hook, a subagent, or pasting the prompt into the root CLAUDE.mdwhy
A slash command saves a fixed prompt so everyone runs exactly the same text, and
$ARGUMENTSfills in the part that changes โ a ticket ID, an environment name. It is committed, so the team shares one version with no retyping. A hook is wrong because it fires on lifecycle events, not when you ask; a subagent adds a separate context for what is just a template; pasting it intoCLAUDE.mdloads release details into every unrelated session. Modern skills can offer the same/name, so it's not only for old command files. - Limit a subagent's tools? Set it where the subagent is defined: frontmatter
tools/disallowedTools, or itsallowedToolswhen you create it. APreToolUsehook on the coordinator can't catch the subagent's calls. โ wrong choice to avoid: a coordinator PreToolUse hook that "intercepts" all subagent tool callswhy
Each subagent is its own instance with its own config, so its tools are decided where it is defined โ the
toolslist in its.claude/agents/frontmatter, orallowedToolspassed at creation. A web-search worker set up that way can't callwrite_report, even if a prompt tells it to. ACLAUDE.mdlist of allowed tools is just advice, and the coordinator'sPreToolUsehook never sees calls inside another agent's session. Separate MCP namespaces would work but need server changes for something config already solves. - Main window filling up? If review or exploration is flooding your main conversation, use a read-only subagent with its own context โ not
/compactmore often. If a pipeline analysis is too shallow, use the Explore subagent, not plan mode. Practice Q&A ยท Q13 ยท โ wrong choice to avoid: running /compact more often, or switching to plan mode to understand the pipelinewhy
A subagent starts with a fresh context window: it can read dozens of files, trace callbacks and inheritance, and return only a short list of findings, so the exploration never lands in your main conversation. Compacting more often still lets the material pile up first and may summarize away other context; a bigger model only delays the problem. The built-in Explore subagent is tuned for thorough discovery, which fixes a shallow three-file summary. Plan mode explores too, but inside the main conversation, and it aims at a change plan, not full discovery.
- Plan first or just do it? Many files and several valid designs? Start in plan mode, then execute. A one-file fix with a stack trace? Just execute directly. The bigger and less clear the change, the more you plan first. โ wrong choice to avoid: starting directly and switching to plan mode only if it gets hard
why
Plan mode lets Claude explore the code, map dependencies and propose a design without editing anything, so a split across dozens of files is designed before the first change. Starting directly assumes you already know the right boundaries, and you pay in rework when cross-module dependencies show up halfway. If you already know it's complex, don't wait until it gets hard to switch to plan mode. For a clear, small change, direct execution is the right start.
- Three instances, one repo? Give each one its own
git worktreeon its own branch.fork_sessioncopies the conversation, not the files. Running all three in the same folder on different branches makes them step on each other. โ wrong choice to avoid: fork_session or a context: fork skill to run all three inside one sessionwhy
git worktreechecks out several branches of one repo into separate folders that share one git history. Each Claude Code instance edits its own files, and you merge with normal branch operations. Three separate clones would work but leave histories to reconcile by hand. One shared folder fails because switching branches changes the files under the other instances.fork_sessionand acontext: forkskill make separate conversations, not separate filesystems, so they can't stop three big edits from colliding.
Hooks
- Where do hooks go? Hooks are settings, not text. Register them under a top-level
hookskey in.claude/settings.json(or local, user or managed settings), never inCLAUDE.md. The script itself lives elsewhere, like.claude/hooks/after_edit.py.why
Hooks can go in
.claude/settings.json(shared),.claude/settings.local.json(personal),~/.claude/settings.json(user) or managed settings, and Claude Code reads them at startup. Writing "always run the formatter" inCLAUDE.mdregisters nothing โ the model may or may not follow it. The JSON only points to a script; the logic lives in that script, which must exist and follow the hook's input/output contract. - How to read a hook entry Read it outside in:
"PostToolUse": [{"matcher": "Write|Edit", "hooks": [{"type": "command", "command": "โฆ"}]}]= event โ matcher on the tool name โ handlers.$CLAUDE_PROJECT_DIRpoints to the repo. Event details arrive as JSON on stdin.why
The event name (
PostToolUse) decides when it fires. Eachmatcheris a regex on the tool name, soWrite|Editcovers both editing tools. The innerhookslist holds the handlers, andtype: commandtells Claude Code to run a shell command.$CLAUDE_PROJECT_DIRexpands to the repo root, so scripts work from anywhere. The handler gets the tool name, inputs and file paths as JSON on stdin โ don't expect a$FILEvariable. - Format after every edit? Use a
PostToolUsehook matchingWrite|Editthat runs the formatter. ACLAUDE.mdsentence or a/formatcommand depends on someone remembering; the hook doesn't. Practice Q&A ยท Q9 ยท Practice Q&A ยท Q44 ยท โ wrong choice to avoid: adding "always run the formatter" to CLAUDE.md, or a manual /format commandwhy
The hook ties the formatter to the event: after any Write or Edit succeeds, Claude Code runs it on the file, whether the model remembered or not. A
CLAUDE.mdrule needs the model to comply on every single edit; a skill runs only when formatting seems relevant;/formatrelies on a human. The hook still misses edits made through Bash or outside Claude Code, so keep a CI formatting check as the repo-wide backstop. - Before or after the call?
PreToolUseruns before the call, and exit code 2 blocks it and sends stderr back to Claude as the reason.PostToolUseruns after, so it can't undo the write.why
Timing is the whole difference. A
PreToolUsehook sees the proposed call before it runs; exit code 2 vetoes it, and the model can't talk its way around that. APostToolUsehook runs after the tool succeeded: it can format, test or log, but the file is already on disk. Both are run by the client, so unlikeCLAUDE.mdtext, they hold even when the model would rather do something else. - Format in PreToolUse? You can't โ when a
PreToolUsehook onWritefires, no file exists yet. UsePreToolUseto check or block inputs, andPostToolUseto fix outputs. โ wrong choice to avoid: a PreToolUse hook on Write that formats the content before the file is writtenwhy
At
PreToolUsetime the Write tool hasn't run, so there is nothing on disk for Checkstyle or Prettier to format; the hook only sees the proposed input. That makes it the place to inspect or reject โ block a write to a protected path, lint a shell command โ not to fix output.PostToolUsegets the finished write and can correct it in place. A hook onReadthat checks files before they enter context is on the wrong event entirely. - Save transcript before compaction? Use
PreCompact. It fires before/compactand before auto-compaction. There is noCompacttool forPreToolUseto match, and aPostToolUsehook on every tool is just noise. โ wrong choice to avoid: a PostToolUse hook on every tool that appends each result to a logwhy
Compaction summarizes the conversation and throws away detail, so anything you want must be saved just before.
PreCompactis the event for that: it fires before manual and automatic compaction and gets the transcript path, so it can copy it to a timestamped log. Compaction is not a tool the model calls, so noPreToolUsematcher catches it. Logging every result withPostToolUsemakes huge files that still miss reasoning and user messages, and a hook onWritefires at unrelated times. - Which event for which job?
UserPromptSubmitadds context or checks the prompt when the user sends it.Stop/SubagentStopenforce "done" criteria.SessionStartloads saved state.why
Match each event to a job.
UserPromptSubmitruns when the user sends a message, before the model sees it.StopandSubagentStoprun when Claude or a subagent finishes โ the place to check that tests passed or the checklist is done, or to send a notification.SessionStartruns once when a session opens. Together withPreToolUse,PostToolUseandPreCompact, these cover the whole session. - What can a hook miss? A
Write|Editmatcher misses edits made through Bash or an outside editor, so back it with a required CI check. And aPreToolUsegate can deny or ask, but it can't wait hours for approval. โ wrong choice to avoid: a PreToolUse hook that pauses and waits for a human approval signalwhy
A hook fires only for the tools its matcher names. A
sedcommand through Bash, a script that rewrites files, or a developer in their IDE never trigger aWrite|Edithook, so the check is silently skipped. The repo-wide guarantee is a required CI check on every pull request, however the change was made. Hooks are also synchronous: aPreToolUsehook can block or ask the user, but it can't pause for an outside approval, so human sign-off belongs in a queue the pipeline routes to.
Headless, CI & the Agent SDK
- Does -p limit Claude? No.
claude -p "โฆ"just runs one prompt without the interactive screen, and--output-format json|stream-jsonmakes the output parseable.-pdoes not restrict file access. If a headless check is shallow, blame the vague prompt, not the flag. โ wrong choice to avoid: thinking -p blocks file reads, or that --output-format json breaks the resultswhy
-p(print mode) runs one prompt and exits โ what CI scripts need โ and--output-format jsonorstream-jsonwraps the result for a pipeline to parse. Neither changes what Claude Code can do: it still has Read, Grep and Glob, and the format doesn't touch the analysis. If a headless check claims 100% coverage that audits disprove, the prompt had no validation criteria, so the model pattern-matched instead of checking each endpoint. Give explicit criteria and an expected output format; a second instance reviewing the same vague output won't help. - Safe CI setup Stack four controls: headless mode +
--allowedTools "Bash(npm run test:*)"+ hooks + read-only credentials.--allowedToolsonly adds pre-approval and never beats a deny.--toolspicks built-in tools and doesn't filter MCP tools.why
Headless mode makes the run scriptable.
--allowedTools "Bash(npm run test:*)"pre-approves exactly the commands the job needs, so it never stalls on a prompt. Hooks add fixed checks around tool calls. Read-only credentials mean even a hijacked prompt can't push or publish. An allow never cancels a deny from any settings layer.--toolsis a different knob: it picks which built-in tools exist and doesn't affect MCP tools, which are set up and denied separately. - What loads in CI? The runner checks out the repo, so the project
CLAUDE.md,.claude/settings.json, rules and skills all load. But~/.claude/CLAUDE.mddoesn't exist there โ nobody put it on that machine. Practice Q&A ยท Q50 ยท โ wrong choice to avoid: "headless mode turns off memory", or "managed policy strips user memory in CI"why
A GitHub Actions runner is a fresh machine with only what the workflow checks out or sets up. Everything committed โ
CLAUDE.md,.claude/rules/,.claude/settings.json,.claude/skills/โ is there and loads normally. A developer's home-folder memory file was never copied, so rules that lived only there vanish in CI. Headless mode doesn't turn off memory, and no managed policy strips user files; the file just isn't on the runner. - @claude did nothing? That's the safety gate working. The
@claudeAction first checks that the commenter has write access. An outside contributor's comment triggers nothing, on purpose, to block prompt injection. It is not a missing API key. Practice Q&A ยท Q49 ยท โ wrong choice to avoid: "the API key is missing from the fork", or "the workflow lacks the comment event"why
Anyone can comment on a public repo, so a workflow that ran on every
@claudemention would let strangers steer an agent that holds the repo's credentials. So the official Action checks the commenter's permission level and runs only for users with write access. The API key lives in the repo's secrets, not in the contributor's fork, and a missing event subscription wouldn't explain why trusted and untrusted commenters behave differently. - What the workflow YAML controls Only three things: WHEN it runs (events +
@claude), WHAT it may touch (permissions:), and WHICH secret (ANTHROPIC_API_KEY)..github/workflows/claude.ymlteaches no conventions โ those all come from the committed files in the repo.why
The workflow file is plumbing, not policy. The
on:block and the@claudetrigger decide when the job runs,permissions:limits the GitHub token, and a secret reference supplies the key. Coding standards, permission rules, hooks and skills all come from the committedCLAUDE.md,.claude/rules/,.claude/settings.jsonand.claude/skills/. On GitHub those committed files are the only voice; there is no user layer to fill gaps. - What may block a merge? Only deterministic checks, like tests and lint, should block a merge. AI review is advice. And adding a workflow file doesn't create a gate by itself โ branch protection must mark the check as required. Practice Q&A ยท Q14 ยท โ wrong choice to avoid: letting Claude's opinion on code quality, or any AI output, block the merge
why
Tests and linters give the same answer every run, so a failure is a repeatable fact the team agreed to enforce. An AI reviewer's opinion is useful, but it isn't proven reliable or objective enough to be a hard gate, so post it as comments. Making any AI output blocking lets unsupported claims stop merges; removing every gate lets known test failures through. And a workflow that runs checks is not a gate until branch protection lists it as required.
- Can deny stop the network? Not fully.
deny: ["Bash(git push *)"]can't stop an allowed script from pushing. In CI you need read-only tokens (permissions: contents: read,persist-credentials: false) plus a sandbox that blocks outgoing network traffic. Practice Q&A ยท Q12 ยท โ wrong choice to avoid: a deny rule on git push or curl alone, or prompt instructionswhy
Permission rules match the command Claude Code is asked to run. So a deny on
git pushorcurldoes nothing when an allowed script likenpm testopens a connection or pushes by itself. The guarantee must come from the environment: a token withpermissions: contents: readandpersist-credentials: false, so no write credential is on the runner, plus a network sandbox or egress (outgoing traffic) policy. Prompt instructions and trusting the model restrict nothing when the model makes a mistake or reads malicious input. - SDK or plain API? The Agent SDK's
query({ prompt, options })takestools,allowedTools,disallowedTools,mcpServers, and loadsCLAUDE.mdbased onsetting_sources/cwd. A plainmessages.createcall never finds local files. โ wrong choice to avoid: thinking a plain Messages API call finds CLAUDE.md, or an undocumented --append-config flagwhy
The Agent SDK is the Claude Code runtime as a library, not a thin wrapper on the Messages API. A
query()call takes the same kinds of options Claude Code uses and, depending oncwdandsetting_sources, reads the project'sCLAUDE.mdand settings; an emptysetting_sourcesturns that off. A directanthropic.messages.createcall sends only what you put in the request โ it has no idea aCLAUDE.mdexists on disk. So when a question names the runtime, check which one before assuming instructions get discovered.
Review sources
Domain 2 lesson and worked explanations ยท Official reference↗
Fact wording rewritten as concrete numbered facts on 25 September 2026. The mock providers' original keys are preserved on their review pages; these recall lines retain the engineering caveats.