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:

  1. Part of the system prompt (built once per request from getSystemContext + attribution + chrome).
  2. A <system-reminder>-wrapped text block in a user message (CLAUDE.md, hook output, attachments).
  3. A “real” user text block (the <command-name> header, the skill body, the user’s typed prompt).
  4. A tool_use / tool_result pair.

The model weights things by recency, structural position, and <system-reminder> framing, not by an authority tier. Two important quirks worth knowing:

  • Hook additionalContext gets folded into tool_result.content. When tengu_chair_sermon is on, both PreToolUse and PostToolUse reminders 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% premature Human: stop” bug (messages.ts:2606-2609).
  • command_permissions is UI-only. The skill’s allowedTools list never goes to the model. normalizeAttachmentForAPI returns [] 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.md exists 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 Skill with {"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 by prependUserContext (api.ts:449-474). The wrapper text is “As you answer the user’s questions…” with # claudeMd and # currentDate as section headers. The “Codebase and user instructions are shown below…” sentence is part of the claudeMd value itself (claudemd.ts:90), not the wrapper.
  • gitStatus lives in the system prompt, not as a user message. Built by getSystemContext and joined key: value style by appendSystemContext (api.ts:437-447).
  • No <command-name> header in the user turn. Unlike the user-typed /commit path, when the model invokes Skill, SkillTool.ts:741-750 filters out the <command-message>-bearing user message from newMessages. The header would be redundant: the SkillTool’s own UI handles display.
  • The tool_result content is just "Launching skill: commit" (SkillTool.ts:856-861). The skill body itself is delivered as a separate text block AFTER the tool_result (via newMessages), 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:
    • PostToolUse is pushed AFTER the tool_result in resultingMessages (toolExecution.ts:1515), so it gets folded in at merge time via mergeUserContentBlocks’s universal smoosh (messages.ts:2631-2646).
    • PreToolUse is pushed BEFORE the tool_result (toolExecution.ts:846), survives the merge as a sibling, then gets caught by the post-pass smooshSystemReminderSiblings (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. smooshSystemReminderSiblings only 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_permissions is gone. It was in newMessages but normalizeAttachmentForAPI returns [] for it (messages.ts:4253). The allowedTools from 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
  • commit is a bundled skill (prompt-type command) that lives at src/skills/bundled/commit/SKILL.md
  • A PreToolUse hook on Bash is configured: prints additionalContext: "Remember to sign commits"
  • A PostToolUse hook on Bash is configured: runs git status -sb and returns it as additionalContext
  • The model decides to call Bash with git commit -am 'fix typo in README'

Phase 1: User input → slash expansion

  1. useTextInput captures /commit fix typo in README.
  2. processSlashCommand detects the / prefix, looks up the commit command in the registry.
  3. getMessagesForPromptSlashCommand (processSlashCommand.tsx:902-912) builds four messages, in this order:
    • Header (isMeta: false, normal user text): the output of formatSlashCommandLoadingMetadata (processSlashCommand.tsx:794):
      <command-message>commit</command-message>
      <command-name>/commit</command-name>
      <command-args>fix typo in README</command-args>
      
      Note the order: <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 from command.getPromptForCommand(args, context). For file-based skills (loadSkillsDir.ts:344-399) this is Base directory for this skill: {baseDir}\n\n{markdownContent} after substituteArguments runs 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_permissions attachment: allowedTools + model from the skill’s frontmatter. Important: this attachment is UI-only. normalizeAttachmentForAPI returns [] for command_permissions (messages.ts:4253), so the model never sees it. It only feeds the local permission system.
  4. addInvokedSkill('commit', skillBody) records the skill so it survives compaction (createSkillAttachmentIfNeeded).
  5. setPromptId(uuid) and startInteractionSpan() fire for telemetry.

Phase 2: First API call

  1. QueryEngine.query() collects messagesForAPI and runs normalizeMessagesForAPI:
    • Per-turn attachments from getAttachments() are appended (gitStatus, currentDate, claudeMd, etc.).
    • prependUserContext injects <system-reminder> wrapped CLAUDE.md/userContext as a leading user message.
    • appendSystemContext adds gitStatus etc. into the system prompt as key: value lines.
    • Consecutive user messages are merged into one user turn.
    • If tengu_chair_sermon gate is on: ensureSystemReminderWrap makes wrapping idempotent and smooshSystemReminderSiblings folds reminders adjacent to tool_result blocks into the tool_result content.
  2. services/api/claude.ts assembles the final system prompt: attribution + CLI prefix + advisor instructions + chrome instructions + system context.
  3. Request is streamed; assistant response comes back with a text block and a tool_use block calling Bash.

Phase 3: Tool execution

  1. toolExecution.ts runs the tool:
    • PreToolUse hook fires. If it returns {additionalContext: "Remember to sign commits"}, that becomes a type: 'additionalContext' entry pushed to resultingMessages (line 846 in toolExecution.ts).
    • The PreToolUse additionalContext is converted to a hook_additional_context attachment, which normalizeAttachmentForAPI wraps in <system-reminder>...</system-reminder> as a user-role text block.
    • Bash executes git commit -am 'fix typo in README'.
    • The tool result is pushed as a tool_result block.
    • PostToolUse hook fires. Same translation: additionalContexthook_additional_context attachment → <system-reminder> wrapped text.
  2. After the tool_result and the two hook reminders are pushed as siblings, smooshSystemReminderSiblings (gated by tengu_chair_sermon) folds the sibling reminder text into the tool_result.content so the model never sees a stray text block after a tool_result. This was the fix for the 92% to 0% premature Human: stop rate.

Phase 4: Second API call

  1. The conversation now has: original user turn → assistant turn (text + tool_use) → user turn (tool_result with reminders folded in).
  2. normalizeMessagesForAPI runs again, the same system prompt is reassembled, and the request goes back to the API.
  3. The assistant produces a final text block (“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_permissions is a UI-only attachment. It’s the 4th message produced by getMessagesForPromptSlashCommand, but normalizeAttachmentForAPI returns [] for it (messages.ts:4253). The model never sees the skill’s allowedTools list. That’s enforced locally by the permission system, not by telling the model.
  • Hook output lives inside tool_result.content. With tengu_chair_sermon ON, PreToolUse and PostToolUse additionalContext reminders are smooshed into the tool_result string, not left as sibling text blocks. This is the fix that dropped premature Human: 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 + createSkillAttachmentIfNeeded preserve 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 additionalContext is 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.