Sample Turn: /commit fix typo in README
A walkthrough of one full turn through Claude Code’s pipeline, showing every lifecycle event and the resulting JSON sent to the Anthropic API.
TL;DR
When you type a slash command, Claude Code does a lot of bookkeeping (skill expansion, attachment collection, hook execution, telemetry, compaction tracking), but on the wire it all collapses into a small handful of plain text blocks inside a few user/assistant messages. There is no special role for skills, hooks, CLAUDE.md, or system reminders. Everything is either:
- Part of the system prompt (built once per request from
getSystemContext+ attribution + chrome). - A
<system-reminder>-wrapped text block in a user message (CLAUDE.md, hook output, attachments). - A “real” user text block (the
<command-name>header, the skill body, the user’s typed prompt). - A
tool_use/tool_resultpair.
The model weights things by recency, structural position, and <system-reminder> framing, not by an authority tier. Two important quirks worth knowing:
- Hook
additionalContextgets folded intotool_result.content. Whentengu_chair_sermonis on, bothPreToolUseandPostToolUsereminders for a tool call end up as appended<system-reminder>paragraphs inside the tool_result string, not as sibling text blocks. This was the fix for the “92% prematureHuman:stop” bug (messages.ts:2606-2609). command_permissionsis UI-only. The skill’sallowedToolslist never goes to the model.normalizeAttachmentForAPIreturns[]for it (messages.ts:4253); it only feeds the local permission system.
Minimal example
A small but realistic case: the user has a project CLAUDE.md, two settings hooks configured to match on the Skill tool (one PreToolUse, one PostToolUse), and types a free-form prompt that the model decides to handle by invoking the Skill tool. Then we look at the wire payload of the second API call, the one made right after the Skill tool returns.
Setup:
~/projects/myapp/CLAUDE.mdexists with project conventions.- Settings:
{ "hooks": { "PreToolUse": [{"matcher": "Skill", "hooks": [{"type": "command", "command": "echo '{\"additionalContext\":\"Remember to verify staged changes first\"}'"}]}], "PostToolUse": [{"matcher": "Skill", "hooks": [{"type": "command", "command": "echo '{\"additionalContext\":\"Skill expansion completed\"}'"}]}] } } - User types:
commit my README fix - Model decides to call
Skillwith{"name": "commit"}.
Wire payload of the second API call:
{
"system": [
{
"type": "text",
"text": "You are Claude Code... \n\ngitStatus: M README.md\ncurrentDate: 2026-04-12"
}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# claudeMd\nCodebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.\n\nContents of /Users/me/projects/myapp/CLAUDE.md (project instructions):\n# myapp\n- Use TDD\n- Sign all commits\n# currentDate\nToday's date is 2026-04-12.\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>"
},
{"type": "text", "text": "commit my README fix"}
]
},
{
"role": "assistant",
"content": [
{"type": "text", "text": "I'll use the commit skill."},
{"type": "tool_use", "id": "toolu_01", "name": "Skill", "input": {"name": "commit"}}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01",
"content": "Launching skill: commit\n\n<system-reminder>\nPostToolUse:Skill hook additional context: Skill expansion completed\n</system-reminder>\n\n<system-reminder>\nPreToolUse:Skill hook additional context: Remember to verify staged changes first\n</system-reminder>"
},
{
"type": "text",
"text": "Base directory for this skill: /Users/me/.claude/skills/commit\n\n# Commit\n\nCreate a well-formatted commit with a smart commit message based on the staged changes.\n\nSteps:\n1. Run `git status` to see staged files\n2. Run `git diff --cached` to see what's being committed\n3. Draft a concise commit message\n4. Create the commit"
}
]
}
]
}Things to notice (each verified against source):
- CLAUDE.md is a
<system-reminder>-wrapped user text block at the start of turn 1. Built byprependUserContext(api.ts:449-474). The wrapper text is “As you answer the user’s questions…” with# claudeMdand# currentDateas section headers. The “Codebase and user instructions are shown below…” sentence is part of theclaudeMdvalue itself (claudemd.ts:90), not the wrapper. gitStatuslives in the system prompt, not as a user message. Built bygetSystemContextand joinedkey: valuestyle byappendSystemContext(api.ts:437-447).- No
<command-name>header in the user turn. Unlike the user-typed/commitpath, when the model invokesSkill,SkillTool.ts:741-750filters out the<command-message>-bearing user message fromnewMessages. The header would be redundant: the SkillTool’s own UI handles display. - The
tool_resultcontent is just"Launching skill: commit"(SkillTool.ts:856-861). The skill body itself is delivered as a separatetextblock AFTER thetool_result(vianewMessages), not embedded in the result. - Both hook reminders are folded into
tool_result.content, not left as siblings. They appear in PostToolUse-then-PreToolUse order. That’s because:PostToolUseis pushed AFTER thetool_resultinresultingMessages(toolExecution.ts:1515), so it gets folded in at merge time viamergeUserContentBlocks’s universal smoosh (messages.ts:2631-2646).PreToolUseis pushed BEFORE thetool_result(toolExecution.ts:846), survives the merge as a sibling, then gets caught by the post-passsmooshSystemReminderSiblings(messages.ts:1835) which appends it.- Smoosh order is
[existing tool_result content, ...new SR blocks]joined by\n\n(messages.ts:2560-2568).
- The skill body stays as a sibling text block, not smooshed.
smooshSystemReminderSiblingsonly folds<system-reminder>-prefixed text (messages.ts:1849). The skill body is plain text: leaving it as a sibling is intentional, since “real user input as a sibling after tool_result is semantically correct” (messages.ts:1827-1831). On the wire, it renders as</function_results>\n\nHuman:\n[skill body], which is exactly the framing the model needs. command_permissionsis gone. It was innewMessagesbutnormalizeAttachmentForAPIreturns[]for it (messages.ts:4253). TheallowedToolsfrom the skill’s frontmatter only feed the local permission system; the model never sees them.
The full walkthrough below shows the user-typed /commit variant (which keeps the <command-name> header and doesn’t go through the Skill tool), so you can compare both invocation paths.
Scenario
- User types:
/commit fix typo in README commitis a bundled skill (prompt-type command) that lives atsrc/skills/bundled/commit/SKILL.md- A
PreToolUsehook onBashis configured: printsadditionalContext: "Remember to sign commits" - A
PostToolUsehook onBashis configured: runsgit status -sband returns it asadditionalContext - The model decides to call
Bashwithgit commit -am 'fix typo in README'
Phase 1: User input → slash expansion
useTextInputcaptures/commit fix typo in README.processSlashCommanddetects the/prefix, looks up thecommitcommand in the registry.getMessagesForPromptSlashCommand(processSlashCommand.tsx:902-912) builds four messages, in this order:- Header (
isMeta: false, normal user text): the output offormatSlashCommandLoadingMetadata(processSlashCommand.tsx:794):
Note the order:<command-message>commit</command-message> <command-name>/commit</command-name> <command-args>fix typo in README</command-args><command-message>first, then<command-name>(with/prefix), then<command-args>, joined by\n. - Body (
isMeta: true, hidden from UI but visible to API): the SKILL.md content fromcommand.getPromptForCommand(args, context). For file-based skills (loadSkillsDir.ts:344-399) this isBase directory for this skill: {baseDir}\n\n{markdownContent}aftersubstituteArgumentsruns and${CLAUDE_SKILL_DIR}/${CLAUDE_SESSION_ID}are interpolated. - Attachment messages:
getAttachmentMessages(..., { skipSkillDiscovery: true })for any @-mentions or MCP resources referenced in the command args / skill body. command_permissionsattachment:allowedTools+modelfrom the skill’s frontmatter. Important: this attachment is UI-only.normalizeAttachmentForAPIreturns[]forcommand_permissions(messages.ts:4253), so the model never sees it. It only feeds the local permission system.
- Header (
addInvokedSkill('commit', skillBody)records the skill so it survives compaction (createSkillAttachmentIfNeeded).setPromptId(uuid)andstartInteractionSpan()fire for telemetry.
Phase 2: First API call
QueryEngine.query()collectsmessagesForAPIand runsnormalizeMessagesForAPI:- Per-turn attachments from
getAttachments()are appended (gitStatus, currentDate, claudeMd, etc.). prependUserContextinjects<system-reminder>wrapped CLAUDE.md/userContext as a leading user message.appendSystemContextadds gitStatus etc. into the system prompt askey: valuelines.- Consecutive user messages are merged into one user turn.
- If
tengu_chair_sermongate is on:ensureSystemReminderWrapmakes wrapping idempotent andsmooshSystemReminderSiblingsfolds reminders adjacent totool_resultblocks into the tool_result content.
- Per-turn attachments from
services/api/claude.tsassembles the finalsystemprompt: attribution + CLI prefix + advisor instructions + chrome instructions + system context.- Request is streamed; assistant response comes back with a
textblock and atool_useblock callingBash.
Phase 3: Tool execution
toolExecution.tsruns the tool:- PreToolUse hook fires. If it returns
{additionalContext: "Remember to sign commits"}, that becomes atype: 'additionalContext'entry pushed toresultingMessages(line 846 intoolExecution.ts). - The PreToolUse
additionalContextis converted to ahook_additional_contextattachment, whichnormalizeAttachmentForAPIwraps in<system-reminder>...</system-reminder>as a user-role text block. Bashexecutesgit commit -am 'fix typo in README'.- The tool result is pushed as a
tool_resultblock. - PostToolUse hook fires. Same translation:
additionalContext→hook_additional_contextattachment →<system-reminder>wrapped text.
- PreToolUse hook fires. If it returns
- After the
tool_resultand the two hook reminders are pushed as siblings,smooshSystemReminderSiblings(gated bytengu_chair_sermon) folds the sibling reminder text into thetool_result.contentso the model never sees a stray text block after a tool_result. This was the fix for the 92% to 0% prematureHuman:stop rate.
Phase 4: Second API call
- The conversation now has: original user turn → assistant turn (text + tool_use) → user turn (tool_result with reminders folded in).
normalizeMessagesForAPIruns again, the same system prompt is reassembled, and the request goes back to the API.- The assistant produces a final
textblock (“Committed.”) and the turn ends.
What the API actually sees (Phase 4 request body)
{
"system": [
{
"type": "text",
"text": "You are Claude Code, Anthropic's official CLI for Claude.\n... <full system prompt> ...\n\ngitStatus: M README.md\nworkingDirectory: /Users/achhina/projects/foo\ncurrentDate: 2026-04-12"
}
],
"tools": [
{"name": "Bash", "description": "...", "input_schema": {}},
{"name": "Read", "description": "...", "input_schema": {}},
{"name": "Edit", "description": "...", "input_schema": {}},
{"name": "Write", "description": "...", "input_schema": {}},
{"name": "Glob", "description": "...", "input_schema": {}},
{"name": "Grep", "description": "...", "input_schema": {}}
],
"messages": [
{
"role": "user",
"content": [
{
"type": "text",
"text": "<system-reminder>\nAs you answer the user's questions, you can use the following context:\n# claudeMd\nCodebase and user instructions are shown below. Be sure to adhere to these instructions. IMPORTANT: These instructions OVERRIDE any default behavior and you MUST follow them exactly as written.\n\nContents of /Users/achhina/.claude/CLAUDE.md (user's private global instructions for all projects):\n# Communication Style\n- Be direct and precise\n...\n# currentDate\nToday's date is 2026-04-12.\n\n IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.\n</system-reminder>"
},
{
"type": "text",
"text": "<command-message>commit</command-message>\n<command-name>/commit</command-name>\n<command-args>fix typo in README</command-args>"
},
{
"type": "text",
"text": "Base directory for this skill: /Users/achhina/.claude/skills/commit\n\n# Commit\n\nCreate a well-formatted commit with a smart commit message based on the staged changes.\n\nSteps:\n1. Run `git status` to see staged files\n2. Run `git diff --cached` to see what's being committed\n3. Draft a concise commit message\n4. Create the commit\n\nARGUMENTS: fix typo in README"
}
]
},
{
"role": "assistant",
"content": [
{
"type": "text",
"text": "I'll commit the fix."
},
{
"type": "tool_use",
"id": "toolu_01ABC",
"name": "Bash",
"input": {
"command": "git commit -am 'fix typo in README'",
"description": "Commit the README typo fix"
}
}
]
},
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01ABC",
"content": "[main abc1234] fix typo in README\n 1 file changed, 1 insertion(+), 1 deletion(-)\n\n<system-reminder>\nPreToolUse:Bash hook additional context: Remember to sign commits\n</system-reminder>\n\n<system-reminder>\nPostToolUse:Bash hook additional context: Status:\n## main\n</system-reminder>"
}
]
}
]
}Key observations
- Three messages, not many. Despite ~30 attachment types, ~5 lifecycle events, and 2 hooks firing, the wire payload collapses to 3 messages (
user,assistant,user). Everything else is folded into text blocks within those messages. command_permissionsis a UI-only attachment. It’s the 4th message produced bygetMessagesForPromptSlashCommand, butnormalizeAttachmentForAPIreturns[]for it (messages.ts:4253). The model never sees the skill’sallowedToolslist. That’s enforced locally by the permission system, not by telling the model.- Hook output lives inside
tool_result.content. Withtengu_chair_sermonON, PreToolUse and PostToolUseadditionalContextreminders are smooshed into the tool_result string, not left as sibling text blocks. This is the fix that dropped prematureHuman:stops from 92% to 0%. - No formal authority hierarchy. The system prompt, user CLAUDE.md, skill body, and hook reminders are all just text. The model weights them by recency, structural position, and
<system-reminder>framing, not by an explicit role tier. - Skill body is
isMeta: true. It doesn’t render in the UI transcript but is structurally a normal user-role text block to the API. That’s why skills “feel overpowering”: they’re recent, large, and indistinguishable from a fresh user instruction. - Skill content survives compaction.
addInvokedSkill+createSkillAttachmentIfNeededpreserve up to 5KB per skill (25KB total) post-compact, while the pre-skill conversation gets summarized away. <command-name>is just a label. It’s the only marker that disambiguates “user typed/commit” from “user typed the commit body verbatim”. The model has no other signal.- PreToolUse
additionalContextis observed after the tool runs. Even though the hook fires before tool execution, the model only sees the reminder in the next API call, alongside the tool_result. PreToolUse can’t actually steer the in-flight tool call; it can only steer the next assistant turn.