Documentation v0.3.2
Earshot / Using Earshot

Headless output

The earshot.v1 JSON and streaming output contract.

Reviewed for 0.3.2 · Latest documentation

earshot -p "<prompt>" runs one non-interactive turn. With --output-format json or stream-json it is an API, and it is versioned as one.

earshot -p "what does the registry do" --output-format json
earshot -p "fix the failing test" --output-format stream-json --permission-mode auto

The version#

Every object earshot writes in a JSON format carries a schema field:

{ "schema": "earshot.v1", "type": "result", "...": "..." }

earshot.v1 is a promise about change, not a version of earshot:

  • Additive changes are allowed within v1. New record types and new fields may appear in any release. A consumer must ignore records whose type it does not recognise and fields it did not expect. That rule is what lets earshot report new things — a hook firing, a subagent finishing — without breaking a script already parsing the stream.
  • Renaming a field, removing one, or changing what one means requires v2.
  • v2 will not arrive by upgrading. It will be requested explicitly, as --output-format json@v2, and v1 will keep working alongside it. You can pin today: --output-format json@v1 is accepted and means exactly what json means, so a script can state which contract it was written against.
  • An output format earshot does not implement is an error and exit code 2, not a silent fall back to prose.

earshot's internal event type is deliberately not what gets written. It is ours to rename; the stream is not.

The full promise, alongside ACP's and a list of the surfaces that carry no promise yet, is in Compatibility.

--output-format json#

One object on stdout when the turn ends.

FieldTypeMeaning
schemastringearshot.v1
typestringAlways result
subtypestringsuccess, error, interrupted, max_steps, budget
isErrorbooleanTrue unless subtype is success
textstringThe assistant's response text for the turn
costUsdnumberSession spend, including any subagents
durationMsnumberWall clock for the run
numMessagesnumberMessages in history when the turn ended
modelstringThe model reference that was requested
permissionModestringThe mode the turn ran under
sessionIdstring?Absent when the session was not persisted
errorobject?{ kind, message } when subtype is error

--output-format stream-json#

Newline-delimited JSON, one object per line, as things happen. The final line is the same result object --output-format json produces — so a consumer that reads only the last line and one that parses a single object are reading the same thing.

typeFields
model_startmodel
texttext (a delta, not the whole response)
reasoningtext
intentcalls, text? — the one-line "why" before a batch; text absent means none was given
tool_usetoolCallId, toolName, input
tool_resulttoolCallId, toolName, isError, output
permissiontool, target, title, reason
usageusage, costUsd
budgetspentUsd, limitUsd, raisedTo?raisedTo absent means the run stopped rather than being given a higher limit
compactedreplaced
scopekind, summary, accepted
verificationcommand, exitCode, output
hookevent, blocked?, problems
subagentdescription, steps, costUsd
errorkind, message, retryable
resultas above

permission carries the reason a call needed approval, not the diff. The full detail of a change is for a person looking at a terminal, and a headless run has nobody to show it to — if it needed approving, it did not run.

Permissions in a headless run#

-p defaults to ask like everywhere else. With no terminal to prompt at, a call that needs approval is refused with an explanation the model can act on, and the run continues. That is deliberate: the alternative is a mode where piping a prompt into earshot silently grants it more than typing the same prompt would.

Scripted use passes --permission-mode explicitly. Deny rules still apply in every mode.

Exit codes#

CodeMeaning
0The turn finished
1The turn failed, or hit the step limit
2Bad usage — unknown model, unknown output format, bad permission mode
3Missing credentials, or session budget reached
4No shell available (Git Bash missing on Windows)
130Interrupted
Something unclear?Suggest a correction on GitHub ↗