Files
superpowers/skills/diagnosing-superpowers/references/claude-code-sessions.md
T
Jesse Vincent 751fd239f8 fix(diagnosing-superpowers): human-prompt filters for injected lines and newer Codex rollouts
The Claude Code "human prompts" jq command counted harness-injected
user-shaped lines as typed. On a real 9,170-line transcript it returned
374 rows, one of them a <task-notification> block. Excluding text that
begins with <task-notification>, <command-name>, <local-command-stdout>,
<system-reminder>, or "This session is being continued" brings that to
76, which matches the human-turn count a careful analyst arrives at by
hand.

The same transcript carries six human prompts that never appear as user
lines at all: queued_command attachments with origin.kind "human", typed
while a turn was already running. Adds a second command for those.

Codex: on a thread_source "user" Codex Desktop rollout at cli_version
0.149.0-alpha.4.1 the user_message command returns nothing, and the
prompts arrive as response_item messages with role "user". The file
already documented that shape for subagent rollouts; it is now general,
with the fallback jq and a note that the fallback also carries injected
blocks (<subagent_notification>, <environment_context>, <skill>,
<recommended_plugins>) that have to be skipped.

Claude-Session: https://claude.ai/code/session_01DyaGKhTXvHNs2JgPhDktz7
2026-08-28 16:13:42 -07:00

106 lines
7.4 KiB
Markdown

# Claude Code session store
Verified against: Claude Code 2.1.247 (transcript `version` field), macOS.
When a field below is missing from the file in front of you, trust the file
and say so in coverage notes.
## Where
- Main transcript: `~/.claude/projects/<cwd-slug>/<sessionId>.jsonl`, where
`<cwd-slug>` is the working directory with every `/` replaced by `-`
(e.g. `/tmp/work``-tmp-work`).
- Subagent transcripts: `~/.claude/projects/<cwd-slug>/<sessionId>/subagents/agent-<agentId>.jsonl`,
each with a sibling `agent-<agentId>.meta.json`
(`agentType`, `description`, `toolUseId`, `spawnDepth`, optional `model`).
- Plugin registry: `~/.claude/plugins/installed_plugins.json` — per plugin:
`installPath`, `version`, `installedAt`, `lastUpdated`, `gitCommitSha`.
- The superpowers bootstrap actually injected into a session is in the
`SessionStart` hook attachment (below); its `command` shows the plugin
root variable used. A dev checkout loaded with `--plugin-dir` will not be
in the registry, so report both the registry entry and the hook evidence.
## Which file is the current session
The most recently modified `.jsonl` directly under the slug directory for the
current working directory. Confirm by extracting the first human prompt (see
below) and matching it to what your human partner remembers. If two files
are close in mtime, show both first prompts and ask.
## Line types
Every line is one JSON object. `type` values seen: `user`, `assistant`,
`attachment`, `system`, plus session-level records (`permission-mode`,
`mode`, `bridge-session`, `last-prompt`, `ai-title`, `atis-latch`,
`pr-link`, `queue-operation`, `relocated`, `worktree-state`).
Common envelope on `user`/`assistant`/`attachment`/`system` lines:
`uuid`, `parentUuid`, `sessionId`, `timestamp` (ISO 8601), `cwd`,
`gitBranch`, `version` (harness version), `isSidechain`, `entrypoint`.
| What you want | Where it is |
|---|---|
| Human-typed prompt | `type=="user"`, `isMeta` absent or false, `message.content` is a string or a list whose first block is `type:"text"`. Lines whose first block is `tool_result` are tool results, not prompts. `<system-reminder>` text inside a prompt is injected, not typed. Text beginning with `<task-notification>`, `<command-name>`, `<local-command-stdout>`, `<system-reminder>`, or `This session is being continued from a previous conversation` is harness-injected too, even though `isMeta` is absent on those lines — exclude them or your human-turn count will be several times too high. |
| Human-typed prompt queued mid-turn | `type=="attachment"`, `attachment.type=="queued_command"`, `attachment.origin.kind=="human"`, text in `attachment.prompt`. These are typed while a turn is running and never appear as standalone `user` lines, so they are missing from the list above. Add them to the timeline. |
| Assistant text / tool calls | `type=="assistant"`, `message.content[]` blocks of `type:"text"` or `type:"tool_use"` (`id`, `name`, `input`). |
| Tool result | `type=="user"`, `message.content[0].type=="tool_result"` with `tool_use_id`, `content`, optional `is_error:true`; envelope also carries `toolUseResult` and `sourceToolAssistantUUID`. |
| Model | `message.model` on assistant lines. |
| Tokens | `message.usage` on assistant lines: `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`. |
| Skill invocation | `tool_use` block with `name:"Skill"` and `input.skill` (e.g. `superpowers:brainstorming`); the tool result line has `toolUseResult.commandName`. |
| Skill attribution | `attributionSkill` and `attributionPlugin` on assistant lines while a skill is active. |
| Subagent dispatch | `tool_use` with `name:"Agent"` (`input.description`, `input.subagent_type`, `input.prompt`); the subagent's own file is matched by `toolUseId` in its `.meta.json`. Subagent lines have `isSidechain:true` and `agentId`. |
| Hook output | `type=="attachment"`, `attachment.type` `hook_success`/`hook_failure`, `attachment.hookName` (e.g. `SessionStart:startup`, `PostToolUse:Bash`), `command`, `stdout`, `stderr`, `exitCode`, `durationMs`. |
| Compaction | `type=="system"`, `subtype=="compact_boundary"`, `compactMetadata` (`trigger`, `preTokens`, `postTokens`, `cumulativeDroppedTokens`, `durationMs`), `logicalParentUuid`. |
| Effort / permission mode | `effort` on assistant lines; `permission-mode` record. |
## Safe extraction
Lines can exceed a megabyte. Never print a whole line. Check size first:
```bash
F=~/.claude/projects/<slug>/<id>.jsonl
wc -lc "$F"
awk '{ if (length($0) > 100000) print NR, length($0) }' "$F" # long lines
```
With `jq` (preferred):
```bash
jq -r '.type' "$F" | sort | uniq -c # line-type census
jq -r 'select(.type=="user" and .isMeta!=true and ((.message.content|type)=="string" or .message.content[0].type=="text"))
| select((.message.content|if type=="string" then . else (.[0].text // "") end)
| test("^(<task-notification>|<command-name>|<local-command-stdout>|<system-reminder>|This session is being continued)") | not)
| "\(input_line_number)\t\(.timestamp)\t\((.message.content|if type=="string" then . else .[0].text end)[0:160])"' "$F" # human prompts
jq -r 'select(.type=="attachment" and .attachment.type=="queued_command" and .attachment.origin.kind=="human")
| "\(input_line_number)\t\(.timestamp)\t\(.attachment.prompt[0:160])"' "$F" # human prompts queued mid-turn; merge with the list above
jq -c 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use")
| {name, id, input: (.input|tostring|.[0:120])}' "$F" # tool calls
jq -r 'select(.type=="assistant") | .message.content[]? | select(.type=="tool_use" and .name=="Skill") | .input.skill' "$F" # skill invocations
jq -c 'select(.type=="assistant") | {ts:.timestamp, model:.message.model, skill:.attributionSkill,
u:(.message.usage|{input_tokens,output_tokens,cache_read_input_tokens,cache_creation_input_tokens})}' "$F" # per-message usage
jq -c 'select(.subtype=="compact_boundary") | {line:input_line_number, ts:.timestamp,
m:(.compactMetadata|{trigger,preTokens,postTokens,cumulativeDroppedTokens,durationMs})}' "$F" # compactions (full compactMetadata also has UUID lists; keep this trimmed)
jq -c 'select(.type=="attachment" and (.attachment.type|startswith("hook"))) | {line:input_line_number, hook:.attachment.hookName, exit:.attachment.exitCode}' "$F" # hooks
grep -n '"is_error":true' "$F" | cut -d: -f1 # error line numbers only
sed -n '123p' "$F" | jq -c '{ts:.timestamp, first:((.message.content // "") as $c
| ($c | if type=="array" then ($c[0] // "") else $c end) | tostring | .[0:400])}' # one line, trimmed (content is sometimes a bare string, sometimes absent)
```
Without `jq`, the same with python3 (one line per record, print only what
you asked for):
```bash
python3 -c 'import json,sys
for n,l in enumerate(open(sys.argv[1]),1):
o=json.loads(l)
if o.get("type")=="assistant":
for b in o["message"].get("content",[]):
if b.get("type")=="tool_use": print(n, b["name"], str(b.get("input"))[:120])' "$F"
```
## Subagents
List `~/.claude/projects/<slug>/<id>/subagents/`. For each `agent-*.meta.json`
print `agentType`, `description`, `model`; the matching `.jsonl` is that
subagent's transcript and follows the same line format. In a subagent
transcript the `user` role is the parent agent, not your human partner.