Package:
@anthropic-ai/claude-code@2.1.73(native Mach-O arm64 build, symlinked from~/.local/share/claude/versions/2.1.73) Date: 2026-03-12 Methods: Static source analysis (prettier-formatted cli.js) + Dynamic wire capture (fetch monkeypatch) + Codex independent review
/btw is a "side question" command that forks the current conversation context, sends a single-turn query, and displays the response inline. The main conversation is not affected. Tool calls are blocked at the client level, but the full tool schema is still sent to the API.
| Source | Method | Analyst | Evidence Files |
|---|---|---|---|
| Static analysis | prettier formatted cli.js (~571K lines), keyword search |
Claude Code | /tmp/cc_cli.js (formatted from npm cache at /opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js) |
| Dynamic wire capture | NODE_OPTIONS="--require hook.js" fetch monkeypatch |
Claude Code | /tmp/btw-captures/{005_NORMAL,006_BTW,009_BTW,011_BTW,013_NORMAL,014_NORMAL}_full.json |
| Independent static review | Binary string extraction from 2.1.73 native build | Codex | Separate report |
Installation type: The claude binary at ~/.local/bin/claude is a native Mach-O arm64 executable (not a Node.js script), symlinked to ~/.local/share/claude/versions/2.1.73. However, it embeds a Node.js runtime — the NODE_OPTIONS="--require ..." hook successfully intercepted globalThis.fetch calls, confirming that the native build uses Node.js internally for API communication.
Wire capture validation: The fetch hook intercepts globalThis.fetch calls inside the process, logs the full JSON body to disk, then forwards the request to the real Anthropic API. Confirmed working: normal messages in the same session received real model responses, and later requests (013, 014) contain tool_use/tool_result blocks from actual agent interactions.
Two independent static artifacts: The prettified npm cli.js (/tmp/cc_cli.js) and the local native Mach-O binary (~/.local/share/claude/versions/2.1.73) serve as independent static evidence sources. The npm artifact confirms the full call chain (QpY → mpY → me4 → hv) with line-level tracing. The native binary independently corroborates key parameters via string extraction: querySource:"side_question", forkLabel:"side_question", maxTurns:1, skipCacheWrite:!0. Neither artifact alone would be sufficient — the npm source provides structural detail, while the native binary confirms the same parameters exist in the actual installed executable.
Reproduction caveat: This capture method is verified only for the native macOS arm64 build of 2.1.73 which embeds a Node.js runtime. It may not work for other distribution formats (e.g., if a future version ships without embedded Node.js). For proxy-based capture (distribution-agnostic), see the alternative method at the end of this document.
All excerpts below are verbatim from the prettier-formatted cli.js (line numbers refer to /tmp/cc_cli.js).
Step 1: Regex match (line 447806)
// cli.js:447806 — verbatim
BpY = /^\/btw\b/gi;Step 2: Command handler (lines 447950-447954)
// cli.js:447950-447954 — verbatim
async function QpY(A, q, K) {
let Y = K?.trim();
if (!Y)
return (A("Usage: /btw <your question>", { display: "system" }), null);
return nw.createElement(mpY, { question: Y, context: q, onDone: A });
}Step 3: React component calls API function (lines 447810, 447847)
// cli.js:447810 — verbatim (abridged, showing the me4 call)
function mpY(A) {
// ... React state setup ...
// line 447847:
m = await me4({
question: K,
cacheSafeParams: {
systemPrompt: y,
userContext: u,
systemContext: S,
toolUseContext: Y,
forkContextMessages: Y.messages, // ← passed to hv(), then filtered by VF1() before sending
},
});Step 4: API call function (lines 447765-447801)
// cli.js:447765-447801 — verbatim
async function me4({ question: A, cacheSafeParams: q }) {
let K = {
...q.toolUseContext,
options: { ...q.toolUseContext.options, maxThinkingTokens: 0 },
},
Y = `<system-reminder>This is a side question from the user. You must answer this question directly in a single response.
CRITICAL CONSTRAINTS:
- You have NO tools available - you cannot read files, run commands, search, or take any actions
- This is a one-off response - there will be no follow-up turns
- You can ONLY provide information based on what you already know from the conversation context
- NEVER say things like "Let me try...", "I'll now...", "Let me check...", or promise to take any action
- If you don't know the answer, say so - do not offer to look it up or investigate
Simply answer the question with the information you have.</system-reminder>
${A}`,
z = await hv({
promptMessages: [g6({ content: Y })],
cacheSafeParams: { ...q, toolUseContext: K },
canUseTool: async () => ({
behavior: "deny",
message: "Side questions cannot use tools",
decisionReason: { type: "other", reason: "side_question" },
}),
querySource: "side_question",
forkLabel: "side_question",
maxTurns: 1,
}),
H = z.messages
.find((O) => O.type === "assistant")
?.message?.content?.find((O) => O.type === "text");
return {
response: H && H.type === "text" ? H.text.trim() : null,
usage: z.totalUsage,
};
}Note on obfuscated names:
me4,QpY,mpY,hv,g6are minified identifiers from the bundled source. The call chainQpY → mpY → me4 → hvis traced by following the code path from the/btwregex match through each function reference. The variable names are minified but the string literals ("side_question", "Side questions cannot use tools", the system-reminder text) are unambiguous anchors.
skipCacheWrite: true is a direct parameter in the /btw call path, confirmed by two independent static artifacts: the raw npm bundle contains WR(... skipCacheWrite:!0) in the side-question helper (cli.js line ~2663), and the native binary's string extraction shows the equivalent qL(... skipCacheWrite:!0). It is not visible in the prettified me4 excerpt above (which does show the hv() call but not this parameter); the raw npm bundle's equivalent side-question helper includes it directly.
Captured via NODE_OPTIONS="--require /tmp/cc-fetch-hook.js" claude, which monkeypatches globalThis.fetch to log all Anthropic API request bodies to disk before forwarding them to the real API.
Capture session: User sent "hello" (normal), then /btw what is this project?. Both requests were captured. The normal request received a real model response, confirming the hook was active and passthrough was working.
From /tmp/btw-captures/006_BTW_full.json, redacted to key fields:
{
"model": "claude-opus-4-6",
"max_tokens": 32000,
"stream": true,
"thinking": { "type": "adaptive" },
"tools": [
{ "name": "Agent" }, { "name": "Bash" }, { "name": "Glob" },
{ "name": "Grep" }, { "name": "Read" }, { "name": "Edit" },
{ "name": "Write" }, { "name": "Skill" }, { "name": "ToolSearch" }
],
"system": "[array, 3 blocks, ~24446 chars]",
"messages": [
{ "role": "user", "content": "<available-deferred-tools>..." },
{ "role": "user", "content": ["(5 text blocks: skills, hooks, CLAUDE.md, 'hello')"] },
{ "role": "assistant", "content": [{ "type": "text", "text": "\n\nHello! How can I help you today?" }] },
{ "role": "user", "content": "<system-reminder>This is a side question...{constraints}...</system-reminder>\n\nwhat is this project?" }
]
}Key observations:
"tool_choice"field is absent (not set tonone, not present at all)"thinking"is{"type":"adaptive"}— not disabled despite source settingmaxThinkingTokens: 0- All 9 tools are present with full schemas (only names shown above for brevity)
- This capture has 4 messages (short conversation). A later BTW capture (
011_BTW) has 9 messages but still only text blocks — notool_use/tool_resultblocks. See "Messages" section below for analysis.
From /tmp/btw-captures/005_NORMAL_full.json:
{
"model": "claude-opus-4-6",
"max_tokens": 32000,
"stream": true,
"thinking": { "type": "adaptive" },
"tools": [
{ "name": "Agent" }, { "name": "Bash" }, { "name": "Glob" },
{ "name": "Grep" }, { "name": "Read" }, { "name": "Edit" },
{ "name": "Write" }, { "name": "Skill" }, { "name": "ToolSearch" }
],
"system": "[array, 3 blocks, ~24472 chars]",
"messages": [
{ "role": "user", "content": "<available-deferred-tools>..." },
{ "role": "user", "content": ["(5 text blocks: skills, hooks, CLAUDE.md, 'hello')"] }
]
}| Field | NORMAL | BTW | Difference |
|---|---|---|---|
| URL | https://api.anthropic.com/v1/messages?beta=true |
Same | None |
| model | claude-opus-4-6 |
claude-opus-4-6 |
None |
| max_tokens | 32000 | 32000 | None |
| stream | true |
true |
None |
| tools | 9 tools | 9 tools (same names, same schemas) | None |
| tool_choice | absent | absent | None |
| thinking | {"type":"adaptive"} |
{"type":"adaptive"} |
None |
| system | ~24472 chars | ~24446 chars | Not byte-identical: diff in cache control ttl field |
| messages | 2 | 4 | BTW has conversation context + btw question appended |
| last message | "hello" |
"<system-reminder>...side question...</system-reminder>\n\nwhat is this project?" |
BTW wraps in system-reminder |
System prompt diff detail: The system prompts are semantically equivalent but not byte-identical. The difference is in cache control metadata: the NORMAL request includes "ttl": "1h" in a cache control block that the BTW request omits. This is an internal caching optimization, not a content difference.
Conclusion from wire data: The only meaningful differences between a NORMAL and BTW API request are:
- BTW includes the current conversation context as a fork (see Messages section for scope nuance)
- BTW appends the question wrapped in
<system-reminder>tags as the last user message
Tools, thinking mode, model, and system prompt content are materially the same (system prompt has a minor cache control metadata difference).
Two layers of defense, but only one is visible on the wire:
| Layer | Mechanism | Visible on wire? | Evidence |
|---|---|---|---|
| Prompt injection | <system-reminder> says "You have NO tools available" |
Yes — in last user message | Wire capture |
| Client-side deny | canUseTool callback returns {behavior: "deny"} |
No — client intercepts before execution | Source (line 447785) |
| API-level restriction | None — full tools array sent, no tool_choice: none |
Confirmed absent | Wire capture |
The model sees tool definitions and is told via prompt not to use them. If it tries anyway, the client blocks execution. This is a belt-and-suspenders approach, not an API-level restriction.
Confirmed by wire capture: context fork. The current conversation context is sent, with the btw question appended as a final user message. The response is not written back to the main conversation.
What the wire data shows:
- BTW requests contain prior user and assistant messages from the current session
- The BTW question is appended as the last user message
- BTW captures (006, 009, 011) contain only text content blocks — no
tool_useortool_resultblocks appear in any BTW capture
What the wire data does NOT prove:
- Whether the full untruncated conversation history is sent (BTW captures were from short sessions with limited prior turns)
- Whether
tool_use/tool_resultblocks would be included in a longer session (they are present in later NORMAL captures 013/014, but no BTW was captured after tool-heavy interaction)
What the source reveals — context assembly with filtering:
forkContextMessages: Y.messages(line 447847) passes the current context object's messages array intome4- Inside
hv()(line 441766),forkContextMessagesis extracted - The final query messages are assembled at line 441769:
Z = [...VF1(P), ...A]— wherePis the fork context andAis the new prompt messages VF1(line 380492) filters the context: it removes assistanttool_useblocks that have no matchingtool_result. This means/btwdoes not send a raw copy of the conversation — it sends a filtered fork where orphaned tool calls are stripped
The VF1 filter proves that the fork context is normalized before sending — it is a known source-level mechanism that removes tool blocks. However, VF1 alone does not fully explain why the current wire samples contain no tool_use/tool_result at all: the BTW captures were taken from sessions with limited prior tool interaction, so the absence may also reflect that few or no tool exchanges existed in the context at capture time.
Fork isolation — response is not written back:
YR(z)(line 441770, defined at 265944) generates a separate agent ID in the formata${forkLabel}-${hex}(e.g.,aside_question-a1b2c3)- The fork's messages are recorded to a separate transcript via
h31(...)(lines 441773, 441804) - This is not just "the response isn't used" — the fork has its own identity and transcript from the start
Additional client-side behaviors (from source, not visible on wire):
forkLabel: "side_question"— tags the fork for internal tracking (source line 447791)skipCacheWrite: true— direct parameter in the/btwcall path, dual static evidence: npm bundle (WR(... skipCacheWrite:!0)) and native binary (qL(... skipCacheWrite:!0))maxTurns: 1— client stops after one assistant response (source line 447792)
Confirmed by wire capture: embedded in the last user message's content field as a plain string with <system-reminder> tags. Not in the system prompt array. The system prompt array is unchanged between NORMAL and BTW requests.
Verbatim from cli.js lines 447970-447984:
// cli.js:447970-447984 — verbatim
((UpY = {
type: "local-jsx",
name: "btw",
description:
"Ask a quick side question without interrupting the main conversation",
isEnabled: () => !1,
isHidden: !1,
immediate: !0,
argumentHint: "<question>",
load: () => Promise.resolve().then(() => (Qe4(), Fe4)),
userFacingName() {
return "btw";
},
}),
(fSA = UpY));isEnabled: () => !1 (false): This does not mean the command is disabled. The isEnabled field controls whether the command appears in the command list used for autocomplete and /help (see line 506532: .filter((D) => D.isEnabled())). Commands with isEnabled: false can still be triggered by direct regex match. Cross-evidence: /files (line 492017) and /tag (line 500698) also have isEnabled: () => !1 and are confirmed functional via direct invocation.
immediate: !0 (true): Executes immediately when the user submits, no confirmation step.
Regex trigger: /^\/btw\b/gi (line 447806)
Traced from source with line references. All function names are minified identifiers.
User types "/btw xxx"
│
├─ Regex /^\/btw\b/gi matches (line 447806)
│
├─ QpY(onDone, context, args) — command handler (line 447950)
│ └─ If empty args → "Usage: /btw <your question>"
│ └─ Otherwise → createElement(mpY, {question, context, onDone})
│
├─ mpY(props) — React (Ink) component (line 447810)
│ │
│ ├─ useEffect: Promise.all([
│ │ qN(tools, model, [], mcpClients), // build system prompt (line 447838)
│ │ xO(), // user context (line 447844)
│ │ IO(), // system context (line 447845)
│ │ ])
│ │
│ └─ me4({ question, cacheSafeParams }) — API call (line 447847 → 447765)
│ ├─ Wraps question in <system-reminder> user message
│ ├─ Calls hv() — main inference loop (line 447765 → 441766), with:
│ │ ├─ maxTurns: 1 (client-enforced)
│ │ ├─ canUseTool: deny all (client-enforced)
│ │ ├─ forkContextMessages → VF1() filters orphaned tool_use (line 441769, 380492)
│ │ ├─ YR() generates fork agent ID: aside_question-{hex} (line 441770, 265944)
│ │ ├─ h31() records to separate fork transcript (lines 441773, 441804)
│ │ └─ Wire result: full tools, adaptive thinking, same system prompt
│ └─ Extracts first text block from assistant response
│
├─ Display response (or error)
│
└─ "Press Space, Enter, or Escape to dismiss" (key handler, line 447824)
The component mpY renders:
/btwlabel (yellow/warning color, bold) + question text (dimmed)- While loading: spinner animation + "Answering..." (warning color)
- On success: response text
- On error: error message (red)
- Dismiss hint: "Press Space, Enter, or Escape to dismiss"
| Aspect | Implementation | Evidence |
|---|---|---|
| Tool definitions on wire | All 9 tools sent (Agent, Bash, Glob, etc.) | Wire: 006_BTW_full.json |
| tool_choice on wire | Absent (not set to none) |
Wire: 006_BTW_full.json |
| Client tool blocking | canUseTool callback returns deny |
Source: line 447785 |
| Thinking on wire | {"type":"adaptive"} (despite source setting maxThinkingTokens: 0) |
Wire: 006_BTW_full.json |
| Message context | Filtered context fork (VF1 strips orphaned tool_use) + btw appended | Source: lines 441769, 380492 + Wire: 006_BTW, 011_BTW |
| System prompt | Semantically identical; minor cache control ttl metadata diff |
Wire: 005_NORMAL_full.json vs 006_BTW_full.json |
| System reminder | <system-reminder> tag in last user message content |
Wire + Source: line 447770 |
| Cache behavior | skipCacheWrite: true — no cache pollution |
Dual static: npm bundle (WR(... skipCacheWrite:!0)) + native binary (qL(... skipCacheWrite:!0)) |
| Turn limit | maxTurns: 1 — client-enforced single response |
Source: line 447792 |
| Side effects on main conversation | None — fork gets separate agent ID (aside_question-{hex}) and transcript |
Source: lines 441770, 265944, 441773 |
| Trigger | Regex /^\/btw\b/gi |
Source: line 447806 |
| Command type | local-jsx, immediate: true |
Source: lines 447971, 447977 |
isEnabled: false |
Not disabled — only excluded from autocomplete/help listing | Source: line 447975 + line 506532 |
| Initial claim | Correction | How discovered |
|---|---|---|
| "Thinking disabled (maxThinkingTokens: 0)" | Wire shows thinking: {type: "adaptive"} — thinking is NOT disabled at API level |
Wire capture |
| "Tools disabled" (ambiguous) | Tools are fully sent in the API request; blocking is client-side only | Wire capture |
| "isEnabled: false means hidden from autocomplete" | More precise: isEnabled controls inclusion in command list filter (line 506532), but does not prevent regex-triggered execution |
Source trace |
| Codex: "still not 100% sure about wire tools" | Confirmed: all 9 tools present, no tool_choice restriction | Wire capture |
| "Full/complete main conversation history" | Context is filtered by VF1 (line 380492) before sending — orphaned assistant tool_use blocks without matching tool_result are stripped | Source trace (Codex round 4) |
# 1. Create fetch hook
cat > /tmp/cc-fetch-hook.js << 'EOF'
const fs = require("fs");
const path = require("path");
const CAPTURE_DIR = "/tmp/btw-captures";
require("fs").mkdirSync(CAPTURE_DIR, { recursive: true });
let reqCount = 0;
const origFetch = globalThis.fetch;
globalThis.fetch = async function(url, options) {
if (options?.body && (String(url).includes("messages") || String(url).includes("anthropic"))) {
reqCount++;
try {
const body = JSON.parse(options.body);
const msgs = body.messages || [];
const last = msgs[msgs.length - 1];
let content = "";
if (last) {
const c = last.content;
if (typeof c === "string") content = c;
else if (Array.isArray(c)) content = c.filter(b => b.type === "text").map(b => b.text).join("\n");
}
const isBtw = content.toLowerCase().includes("side question");
const label = isBtw ? "BTW" : "NORMAL";
const n = String(reqCount).padStart(3, "0");
fs.writeFileSync(path.join(CAPTURE_DIR, `${n}_${label}_full.json`), JSON.stringify(body, null, 2));
fs.writeFileSync(path.join(CAPTURE_DIR, `${n}_${label}_summary.json`), JSON.stringify({
request_number: reqCount, model: body.model, stream: body.stream,
tools_count: (body.tools||[]).length, tool_choice: body.tool_choice,
messages_count: msgs.length, thinking: body.thinking, is_btw: isBtw,
}, null, 2));
} catch(e) {}
}
return origFetch.call(this, url, options);
};
EOF
# 2. Launch Claude Code with hook
rm -rf /tmp/btw-captures && mkdir -p /tmp/btw-captures
NODE_OPTIONS="--require /tmp/cc-fetch-hook.js" claude
# 3. In Claude Code: send "hello", then "/btw what is this?"
# 4. Check captures:
ls /tmp/btw-captures/
cat /tmp/btw-captures/*_BTW_summary.json
# Compare with normal: cat /tmp/btw-captures/*_NORMAL_summary.json