Repository navigation
Pass every Claude goal to the CLI as a structured message - #2070
Merged
Merged
Conversation
The Claude harness wrote the goal to the CLI's stdin as text. On that channel the pinned 2.1.263 CLI acts on the goal before the model sees it, in plan mode as in bypass. Every @path after start of text, whitespace (JavaScript's \s, so a BOM, NBSP, U+3000 and U+2028 count) or CJK punctuation is read into the model request and the session transcript: absolute and ~ paths, directory listings, symlinks out of the workspace. A goal that opens with /word runs as a command: /security-review runs git without the CLI's own hardening, /heapdump writes a heap snapshot holding the run's tokens, and an unknown word ends the run with exit 0, is_error false and no turn, which the run records as Completed. Goals carry text from pull requests, repositories and other models, and plan-mode reviewers are no exception. The CLI parses mentions and commands out of the last text block of a stream-json user message only. BuildInvocation, the one place every Claude prompt is built (fresh, --resume, revise, reviewer), now passes --input-format stream-json and writes one NDJSON user message: the goal as its first block, byte for byte, and a constant trailer as the last. Escaping the sigils instead would change what the model reads and has to copy the CLI's \s exactly; a .NET \s port misses the BOM and the file is read anyway. The message is serialized with relaxed escaping, because the launch pipe measures and re-encodes stdin with its own JSON encoder: non-ASCII text is escaped once, by the pipe. What the message must escape itself (quotes, backslashes, controls, characters outside the BMP) is escaped again on the pipe and costs at most twice the goal's pipe size. A blank goal is refused: in a structured message the CLI drops a block its JavaScript trim() empties, U+FEFF included, and runs the model on the trailer alone, reporting success. The block order is undocumented, so a real-CLI E2E pins it in plan and bypass, fresh and resumed, against a text-channel positive control that must read the same planted secrets, or run the same slash word as its own command. The fake CLIs that serve the Claude dialect decode the message with a shared awk reader, pinned against the real encoder. The --append-system-prompt and Stop-hook reason channels were probed and are inert to @ and /.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
@pathin the goal into the model request and the session transcript: absolute and~paths, directory listings, symlinks out of the workspace, and~/.mcp.json, which under bubblewrap is the run's MCP declaration and token. It also runs a goal that starts with/wordas a command:/security-reviewruns git, and an unknown word ends the run with exit 0 and no model turn, which the run records as Completed.ClaudeCodeHarness.BuildInvocation(backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeCodeHarness.cs) now passes--input-format stream-jsonand writes one NDJSON user message. The goal is the first text block, byte for byte; the constantBegin with the task above.is the last. The CLI parses mentions and commands only from the last block. Fresh,--resume, revise and reviewer prompts all take this path.trim()empties (U+FEFF and every Zs included), then runs the model on the trailer alone and reports success.IsBlankToTheClitreats .NET whitespace plus U+FEFF as blank. Invisible characters the CLI keeps (U+200B, U+3164) are still sent.Test plan
trim()removes; U+200B/U+3164 still sent; non-ASCII escaped once and JSON-escaped characters bounded at 2x on the pipe; launch preflight measures the message. Full unit suite: 11755 passed, 1 skipped.GoalChannelE2ETests(real CLI 2.1.263, stub model on loopback, fake secrets): mentions, resumed session,/fix,/security-review. 4/4 in plan mode and 4/4 in bypass on macOS. The text-channel positive control reads all 14 planted secrets. For a slash word it must reach its result with no model turn and record the word as its own command; mutations with empty control stdin or an unknown control flag turn it red.sandbox-isolationlanes: root (bwrap, floor 96) and non-root uid 1654 (floor 15), each with the four[goal-channel-e2e] ranmarkers