The problem
When I use ReadFile, Find, Grep, or SearchFiles, the content comes back as JSON — string values inside a JSON structure. That means every piece of content I read has been through JSON string encoding before I see it.
This causes real problems. When a file contains \.ts$ (a regex with a literal backslash), JSON encodes that backslash as \\, so I see \\.ts$ in the tool output. I reason about the wrong thing: I think the file has two backslashes when it only has one. This is exactly what happened when reviewing Find.ts — I read the input_examples field, saw \\.ts$, concluded the file was correct, and missed that it was actually \.ts$ (one backslash, wrong escape) in the source.
The double-escape problem is a symptom of a deeper issue: I am not reading the file. I am reading a JSON representation of the file.
Where JSON earns its keep
JSON is the right format when output contains multiple distinct fields the system needs to act on differently — a patchId to pass to EditFile, an error.code to branch on, an approval state to act on. Named fields with typed values.
For text and search tools, there are no such fields. The content is the entire output. JSON adds an encoding layer with no payoff.
Design direction: pipe-aware rendering
The right mental model is Unix stdout: while data is in a pipe it carries structure, but once it hits the screen it’s flat text. The structure is an implementation detail of the pipe — it was never meant to land in Claude’s context window.
- In a pipe (
Pipe chaining Find → Grep etc.): tools pass typed output to the next step directly. No serialisation in between.
- At the display boundary (the result that enters Claude’s context): output is rendered to flat readable text.
defineTool could take an optional render(output) => string alongside handler. The framework calls render for display and passes raw typed output to the next pipe step. Pipe skips render on intermediate steps and only calls it on the final output.
Affected tools
| Tool |
Current |
Better |
ReadFile |
JSON with values array |
Plain text with line numbers |
Find |
JSON with values array |
Newline-separated paths |
Grep |
JSON lines |
path:line:content text (already close) |
SearchFiles |
JSON lines |
Same as Grep |
PreviewEdit |
JSON with diff + patchId |
Diff as plain text, patchId as a labelled field |
Tools like EditFile, CreateFile, Exec return structured results with distinct fields the system acts on — those stay as-is.
The problem
When I use
ReadFile,Find,Grep, orSearchFiles, the content comes back as JSON — string values inside a JSON structure. That means every piece of content I read has been through JSON string encoding before I see it.This causes real problems. When a file contains
\.ts$(a regex with a literal backslash), JSON encodes that backslash as\\, so I see\\.ts$in the tool output. I reason about the wrong thing: I think the file has two backslashes when it only has one. This is exactly what happened when reviewingFind.ts— I read theinput_examplesfield, saw\\.ts$, concluded the file was correct, and missed that it was actually\.ts$(one backslash, wrong escape) in the source.The double-escape problem is a symptom of a deeper issue: I am not reading the file. I am reading a JSON representation of the file.
Where JSON earns its keep
JSON is the right format when output contains multiple distinct fields the system needs to act on differently — a
patchIdto pass toEditFile, anerror.codeto branch on, an approval state to act on. Named fields with typed values.For text and search tools, there are no such fields. The content is the entire output. JSON adds an encoding layer with no payoff.
Design direction: pipe-aware rendering
The right mental model is Unix stdout: while data is in a pipe it carries structure, but once it hits the screen it’s flat text. The structure is an implementation detail of the pipe — it was never meant to land in Claude’s context window.
PipechainingFind→Grepetc.): tools pass typed output to the next step directly. No serialisation in between.defineToolcould take an optionalrender(output) => stringalongsidehandler. The framework callsrenderfor display and passes raw typed output to the next pipe step.Pipeskipsrenderon intermediate steps and only calls it on the final output.Affected tools
ReadFilevaluesarrayFindvaluesarrayGreppath:line:contenttext (already close)SearchFilesPreviewEditdiff+patchIdpatchIdas a labelled fieldTools like
EditFile,CreateFile,Execreturn structured results with distinct fields the system acts on — those stay as-is.