[BUG] `claude -p` / `claude --print` runs are classified as `sdk-cli` and diverge from TUI request scaffold
Context note: This is a frontend/product-boundary parity issue, not a criticism
of any individual implementation. Since -p / --print behavior is already a
known pain point, and upcoming mid-June frontend-facing changes may depend on
predictable CLI/TUI parity, this should be evaluated as a user-facing parity
risk. The goal is to reduce verification time and avoid customer-facing
regressions with potential business impact.
Summary
In Claude Code 2.1.141, claude -p and other non-TTY CLI invocations are not
sent as non-interactive Claude Code CLI sessions. They are classified assdk-cli, which changes the API-visible request scaffold compared with the
interactive TUI path.
This is subtle because the final user prompt block can remain clean. The
problem is the surrounding scaffold: product identity, request attribution, beta
flags, tool contracts, and default system prompt text differ for the same prompt.
Why this matters
-p is used in frontend-facing workflows, CI, evaluation loops, scripts,
scheduled jobs, and internal developer tooling. Users reasonably expect it to
be a machine-readable output mode for Claude Code. If the TUI and -p send
different request scaffolds:
- incidents reproduced manually in TUI are not reproducing the printed run
- evals using
-pmay not measure TUI Claude Code behavior - hooks, skills, MCP, permissions, auth, and tool behavior can drift between
manual and printed workflows
- support teams receive conflicting reports from interactive and print-mode use
- users cannot audit the difference from stdout alone
LLM context is product behavior. Product identity and tool contracts are not
cosmetic metadata; they influence model behavior and user trust.
Version
- Claude Code wrapper package:
@anthropic-ai/claude-code@2.1.141 - Native package:
@anthropic-ai/claude-code-linux-x64@2.1.141 - Native build time in bundle:
2026-05-13T21:26:59Z - Native git SHA in bundle:
4f4623ddd339e1c1b87d659b7c9eb3b66397e7a3
Reproduction
Run Claude Code against a local recorder that captures /v1/messages and
returns an intentional 400. This avoids contacting the real API.
scripts/capture-anthropic.py --port 18082 --out tmp/capture/print.jsonl
Then:
env \
ANTHROPIC_API_KEY=sk-ant-capture \
ANTHROPIC_BASE_URL=http://127.0.0.1:18082 \
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \
CLAUDE_CODE_DISABLE_AUTOUPDATER=1 \
claude -p "TRACE_PROMPT_ALPHA" \
--model claude-sonnet-4-5 \
--output-format json \
--debug-file tmp/capture/print-debug.log
Run the TUI against the same recorder and submit TRACE_PROMPT_ALPHA.
Ignore the TUI's first generate_session_title request; compare -p against
the TUI repl_main_thread request.
Rediscovery track
This can be rechecked across minified releases without relying on local symbol
names:
- Capture a
claude -p/claude --printrequest and a TUI main-thread
request from the same repo, config, model, and prompt state.
- Select comparable packets by debug source:
sdkfor print mode,
repl_main_thread for the TUI main request.
- Compare
User-Agent, the first attributionsystemblock, the identity
system block, anthropic-beta, tools[].name, top-level body keys, and
the final user content block.
- If source inspection is needed, search stable literals:
CLAUDE_CODE_ENTRYPOINT, sdk-cli, --print, process.stdout.isTTY,
User-Agent, and the Claude Code / Agent SDK identity prompt strings.
Observed packet deltas
| Field | claude -p | TUI main-thread request |
| --- | --- | --- |
| debug source | source=sdk | source=repl_main_thread |
| User-Agent | claude-cli/2.1.141 (external, sdk-cli) | claude-cli/2.1.141 (external, cli) |
| attribution system block | cc_entrypoint=sdk-cli | cc_entrypoint=cli |
| identity system block | You are a Claude agent, built on Anthropic's Claude Agent SDK. | You are Claude Code, Anthropic's official CLI for Claude. |
| beta header | lacks redact-thinking-2026-02-12 | includes redact-thinking-2026-02-12 |
| tools | 23 tools, including Glob, Grep, TodoWrite | 24 tools, including TaskCreate, TaskGet, TaskList, TaskUpdate |
Matched fields in the simple capture:
- same model
- same
thinking - same
context_management - same user content block order after matching CLAUDE.md include approval state
- final user prompt block is clean
So this is not primarily a bad token in user content. It is request scaffold
misclassification.
Matrix coverage
The same sdk-cli scaffold appears across:
claude -p "prompt" --output-format textclaude -p "prompt" --output-format jsonclaude -p "prompt" --output-format stream-json --verbose- no
-pbut stdout not a TTY - stdin-only prompt
- positional prompt plus stdin
--input-format stream-json- replayed stream JSON input
--append-system-prompt--system-prompt--exclude-dynamic-system-prompt-sections--tools ""--json-schemaCLAUDE_CODE_ENTRYPOINT=cli claude -p ...
Every covered row still sent source=sdk / sdk-cli.
Suspected cause
In the minified linux-x64 bundle:
- startup computes one non-interactive boolean from
-p,--init-only,
--sdk-url, and !process.stdout.isTTY
- the entrypoint setter maps that boolean to
sdk-cliorcli - the identity prompt selector keys SDK identity off
isNonInteractive - User-Agent and request attribution serialize
CLAUDE_CODE_ENTRYPOINT
This collapses two separate concepts:
- headless CLI output mode
- Agent SDK entrypoint / SDK transport
Expected behavior
Plain claude -p "prompt" should remain a Claude Code CLI session with
non-interactive output. It should preserve CLI identity and request scaffold
unless the caller is actually using an SDK entrypoint.
Suggested fix
Split the current classifier into separate concepts:
isNonInteractive: controls TUI suppression, output formatting, setup prompt
skipping, and other UI concerns
isSdkEntrypoint: controlsCLAUDE_CODE_ENTRYPOINT=sdk-cli, SDK identity,
SDK request attribution, and SDK-specific tool/agent surfaces
Then make -p and stdout piping set isNonInteractive=true without implyingisSdkEntrypoint=true.
Related issues
Filed split for focused tracking:
- Entrypoint identity: https://github.com/anthropics/claude-code/issues/59106
- Tool/default prompt contract: https://github.com/anthropics/claude-code/issues/59107
- Control-plane parity: https://github.com/anthropics/claude-code/issues/59108
- Docs and mitigation routing: https://github.com/anthropics/claude-code/issues/59109
This may explain why many -p/headless issues do not reproduce in TUI:
- #18131 - API 400 tool-use concurrency issue only in print mode
- #20508 - duplicate
tool_useIDs in-p - #33343 - hooks and
--allowedToolsnot enforced in headless-p - #36570 - skills do not trigger in
claude -p - #44756 - skills silently fail in
--print/-pcombinations - #32184 - skill triggering recall is 0% in headless mode
- #39453 -
-pcannot use existing Claude Pro login - #56540 - non-TTY
-phangs with parallel Task fan-out - #52917 -
-pexits before background subagents complete - #55802 - startup launches many headless
sdk-clisessions
Mitigation while upstream is fixed
claude-p demonstrates a safer workaround class:
- https://github.com/smithersai/claude-p
- npm:
claude-p@0.0.4
It drives the real interactive Claude Code TUI through a PTY, injects the prompt
via hooks, reads the transcript after Stop, and emits claude -p-compatible
output. That preserves the TUI scaffold by construction and avoids rewriting API
packets.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗