8 條 jq 指令如何從編碼智能體逐字稿查閲實際操作
8 jq Commands That Show What Your Coding Agent Actually Did, From Its Transcript
文章整理 8 條 jq 指令,從 Claude Code 的 JSONL 工作階段紀錄查閲執行命令、修改檔案、工具錯誤、權限拒絕、耗時、失敗後回應及模型;另介紹 Codex 查詢工具呼叫、模型與 token 總量的方法。作者指出,兩家供應商均未把這些檔案格式列為穩定版本格式,指令按文件欄位及公開樣本編寫,作者並未親自執行,建議先查看自己的檔案再使用。
- Book: LLM Observability Pocket Guide
- Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go
- My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools
- Me: xgabriel.com | GitHub
Forty minutes of scrollback sit above the line that says the refactor is done and the tests pass. Did the agent run git push somewhere in there? Did it touch the migrations folder? Did half its tool calls fail before one worked?
You don't have to scroll. Claude Code writes every session to a JSONL file under ~/.claude/projects/: "messages, tool calls, and tool results", in plaintext, kept for 30 days by default through cleanupPeriodDays. Codex writes rollout files under ~/.codex/sessions (openai/codex discussion #46774). Both are one JSON object per line, which is exactly what jq is good at.
Below are eight jq commands: seven for Claude Code, one set for Codex, each next to a trimmed sample of the lines it reads. Neither vendor documents these files as a stable, versioned format, and I haven't run these commands myself: they're built from the documented fields and the public samples linked below. So start with the first block and look at your own file before you trust any of them.
Find the file, then look at one line
# newest Claude Code session, any project
T=$(ls -t ~/.claude/projects/*/*.jsonl | head -1)
# newest Codex rollout
R=$(ls -t ~/.codex/sessions/*/*/*/rollout-*.jsonl | head -1)
# what kinds of lines are in it
jq -r '.type' "$T" | sort | uniq -c | sort -rn
Each project gets a folder named after its working directory, with every character that isn't a letter or digit turned into - (docs). Replace the first * with *my-repo* to stay inside one repo.
A tool call arrives in an assistant line. Pretty-printed and trimmed to the fields the commands use, it looks like this:
{"type": "assistant",
"timestamp": "2026-10-14T10:01:39.120Z",
"message": {
"id": "msg_01...", "model": "claude-opus-5-5",
"content": [
{"type": "tool_use", "id": "toolu_01...",
"name": "Bash", "input": {"command": "npm test"}}]}}
Its result comes back later in a user line, matched by tool_use_id (this walkthrough explains the pairing):
{"type": "user",
"timestamp": "2026-10-14T10:01:41.002Z",
"message": {"role": "user", "content": [
{"type": "tool_result", "tool_use_id": "toolu_01...",
"is_error": true,
"content": "Permission to use Bash with command ..."}]}}
Three things to know before you start. Subagents write their own transcripts, in the same format, under <session>/subagents/ (docs); when that folder exists, pass "$T" "${T%.jsonl}"/subagents/*.jsonl to any command below to include them (with no match, zsh aborts the command and bash hands jq the literal pattern). The hooks reference says the transcript "is written asynchronously and may lag" (hooks), so on a running session the newest line can be incomplete. For the per-line commands, jq -R 'fromjson? | ...' skips a line that won't parse; for the -s commands, swap -s for -Rn and start the filter with [inputs | fromjson?] |. And the file holds whatever passed through a tool, .env values included (docs), so read the output before you paste it into an issue.
What it ran and what it touched
1. Every shell command it ran
jq -r 'select(.type=="assistant") | .message.content[]?
| select(.type=="tool_use" and .name=="Bash")
| .input.command' "$T"
command is the Bash tool's input field in the hooks reference. This lists what the agent asked to run, including commands you denied and commands that failed. Commands 3 and 4 tell those apart.
2. Every file it edited or wrote
jq -r 'select(.type=="assistant") | .message.content[]?
| select(.type=="tool_use")
| select(.name | IN("Edit", "Write"))
| .input.file_path' "$T" | sort | uniq -c | sort -rn
Both tools take a file_path (hooks reference). The count shows which files it kept coming back to. What this misses: anything changed through Bash, such as a sed -i, a formatter or a codegen script. For those, git status and git diff are the source of truth.

A transcript is one line per event. jq pulls out the few lines that answer your question.
What went wrong
3. Every tool call that came back as an error
jq -rs 'def b: .message.content // [] | arrays | .[];
(map(b | select(.type=="tool_use") | {(.id): .name})
| add) as $n
| .[] | b | select(.type=="tool_result" and .is_error)
| [($n[.tool_use_id] // "?"),
(.content | tostring | .[0:100])] | @tsv' "$T"
-s reads the whole file into one array (jq manual), which lets the first pass build a map from call ID to tool name. The second pass prints the tool name next to the first 100 characters of each error. A long run of errors on one tool is usually where the session went sideways.
4. Permission denials
jq -r 'select(.type=="user") | .message.content[]?
| select(.type=="tool_result" and .is_error)
| .content | tostring
| select(test("has been denied"))' "$T"
A command blocked by your permission rules comes back with text like "Permission to use Bash with command ls ~/private-dir/ has been denied." (anthropics/claude-code #45200). In auto mode there's a better record going forward: a PermissionDenied hook receives the reason, such as [Irreversible Local Destruction] (hooks reference).
5. The five slowest tool calls
jq -rs 'def b: .message.content // [] | arrays | .[];
def ms: (.[0:19] + "Z" | fromdateiso8601) * 1000
+ (.[20:23] | tonumber? // 0);
(map(.timestamp as $t | b | select(.type=="tool_use")
| {(.id): {t: ($t | ms), name, input}}) | add) as $c
| [.[] | .timestamp as $t | b
| select(.type=="tool_result")
| $c[.tool_use_id] as $u | select($u)
| {s: ((($t | ms) - $u.t) / 1000), name: $u.name,
what: ($u.input | tostring | .[0:60])}]
| sort_by(.s) | reverse | .[:5][]
| [.s, .name, .what] | @tsv' "$T"
The transcript has no duration field, so this subtracts the call's timestamp from its result's. The ms helper cuts the milliseconds off before fromdateiso8601 and adds them back, so the math doesn't depend on how your jq build handles fractional seconds. The gap also includes the time the call sat on a permission prompt waiting for you. If you want clean numbers from now on, PostToolUse hooks receive duration_ms, which "excludes time spent in permission prompts" (hooks reference).
Why it changed course, and which model did the work
6. What it said right after each failure
There is no field for "changed approach". The closest thing is the first thing the agent wrote after a tool call failed, because that is usually where it explains the new plan.
jq -rs 'def b: .message.content // [] | arrays | .[];
[.[] | .type as $r | b
| if .type == "tool_result" and .is_error then null
elif $r == "assistant" and .type == "text" then .text
else empty end]
| . as $s | range(1; length)
| select($s[. - 1] == null and $s[.] != null)
| "--- \($s[.][0:300])"' "$T"
The first pass builds a list with a null for every failed call and the agent's text for everything it said. The second prints each piece of text that follows a failure. Read three or four of these and you usually know whether it adapted or kept retrying the same thing.
7. Which model answered, counted once per response
jq -rs 'map(select(.type=="assistant"))
| unique_by(.message.id) | group_by(.message.model)[]
| [.[0].message.model, length] | @tsv' "$T"
The unique_by matters. One API response can be written as several lines that share a message.id and the same usage object, so counting lines over-counts. I went through that in Summing Claude Code's Transcript Usage?, and ccusage dedupes on message.id plus requestId for the same reason. Run it on the subagent files too: a subagent definition can set its own model (subagent frontmatter).
8. The same questions for Codex
A Codex rollout line has a timestamp, an outer type and a payload. The outer types include session_meta, turn_context, response_item and event_msg (toolpath-codex types, which mirror the upstream Rust types). Trimmed, with most values elided. The function_call payload is copied from a fixture in that crate's source:
{"timestamp": "...",
"type": "turn_context",
"payload": {"cwd": "...", "model": "..."}}
{"timestamp": "...", "type": "response_item",
"payload": {"type": "function_call",
"name": "exec_command",
"arguments": "{\"cmd\":\"pwd\"}",
"call_id": "call_1"}}
{"timestamp": "...", "type": "event_msg",
"payload": {"type": "token_count", "info": {
"total_token_usage": {"input_tokens": ...,
"cached_input_tokens": ..., "output_tokens": ...,
"reasoning_output_tokens": ..., "total_tokens": ...}}}}
For Codex:
# model per turn
jq -r 'select(.type=="turn_context") | .payload.model' "$R" \
| sort | uniq -c
# every tool call, name first
jq -r 'select(.type=="response_item") | .payload
| select(.type=="function_call" or .type=="custom_tool_call")
| [.name, ((.arguments // .input) | .[0:100])] | @tsv' "$R"
# cumulative token totals at the end of the session
jq -c 'select(.payload.type == "token_count")
| .payload.info.total_token_usage // empty' "$R" | tail -1
model is a field of the turn context in Codex's protocol source, and total_token_usage includes input_tokens, cached_input_tokens, output_tokens, reasoning_output_tokens and total_tokens in the same file. Tool calls come in two shapes: function_call with arguments as a raw JSON string, and custom_tool_call with a free-form input, such as an apply_patch body (toolpath-codex types). Tool names and argument keys change between Codex releases, so list the names first. Then parse .arguments | fromjson for the tool that runs shell commands in your version, and look for the command under cmd (as in the fixture above) or command.
Where Hermes fits
I build Hermes IDE, so weigh this section with that in mind. Hermes runs Claude Code, Codex and other agents side by side, each in its own terminal and its own worktree. When an agent doesn't report its own cost, Hermes estimates spend from that agent's transcript. Each session shows "the cost an agent reports, or an estimate from its transcript clearly marked as estimated, with token totals" (README).

Hermes 2.3.4: Claude Code's /context breakdown, the per-session context gauges in the sidebar, and the status strip with the token total and estimated cost.
Conceptually, an estimate like that is close to command 7's dedupe plus each response's token usage summed per model, with a price list on top. The counting is where it gets hard. Hermes 2.2.9's release notes include: "Cost and token totals no longer grow while a session sits open: each session's usage is counted once." The jq commands above stay useful next to it. Hermes answers how much a session cost; the transcript answers what the agent did. For what changed on disk, Hermes saves every agent turn, and the Review Desk (⌘G, Ctrl+Shift+G on Windows and Linux) shows the changes by turn or by file.

Hermes 2.3.4: Codex and Claude Code side by side, each in its own hermes/ worktree, with the sessions sidebar showing status, context use, cost and branch.
Hermes is free to download, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Anthropic, OpenAI or Nous Research. Which of the eight questions do you ask most after a long session? Tell me in the comments, along with the one you'd add.
If this was useful
If you run more than one agent, Hermes IDE puts them in one window with spend per session, and a star on GitHub helps other developers find it.
These eight commands are tracing by hand: one session, one file, one question at a time. The LLM Observability Pocket Guide is about doing that for a whole team, and about picking the tracing and evals tools that make it routine instead of a jq session.
來源:DEV Community · dev.to
