csift
Reads, searches and counts Claude Code session transcripts (the .jsonl files under ~/.claude/projects, subagents too) with the csift command-line tool, and ships hooks that re-inject compacted turns or record pending questions. Use when asked what a past or running session said, did, edited, spent or is doing now; when about to grep, jq or script over a transcript jsonl (a type=="user" filter overcounts the person's messages), count how many messages the person typed, tool calls or tokens, or claim something was never done or decided; to rebuild a lost file or plan from what a session wrote, extract pasted images, restore turns a compaction summary lost, find a subagent's own id when CLAUDE_CODE_SESSION_ID names the parent, wait on or message another session or subagent; or when csift prints "unknown label selector", "show needs an address", "looks like a session/agent id, not a project path" or "0 matches — a DEFINITIVE absence". Not for semantic or embedding search: csift matches regular expressions only.
含まれるファイル(19)
- SKILL.md24.6 KB
- references/channel.md15.6 KB
- references/disk.md23.6 KB
- references/errors.md25.7 KB
- references/hooks.md20.0 KB
- references/image.md6.5 KB
- references/labels.md28.8 KB
- references/lanes.md19.9 KB
- references/live.md17.5 KB
- references/provenance.md12.6 KB
- references/ranges.md5.3 KB
- references/search.md40.4 KB
- references/sessions.md13.2 KB
- references/shapes.md14.6 KB
- references/survival.md11.1 KB
- references/verbatim.md11.8 KB
- scripts/compact-slice.sh1.8 KB
- scripts/elicitation-marker.sh6.7 KB
- scripts/taskstop-teammate.sh5.3 KB
SKILL.md(原文)
インストールする前に、エージェントに与えられる指示の中身を確認できます。
csift
csift reads Claude Code session transcripts (~/.claude/projects/<encoded-cwd>/<session-uuid>.jsonl, their subagents/ transcripts and sidecar files) and answers with labelled records, counts and addresses you can re-run. The failure it prevents: a hand-written grep, jq or script over that format returns a plausible answer that is silently wrong, for the reasons law 1 lists.
Surface: csift 0.13.0 (must equal the output of csift --version, which prints csift 0.13.0). csift <command> --help is the authority on flag names and syntax, and when this skill and a help page disagree on those, the help page wins; on a behaviour this skill measured, the measured line wins, because a help page describes intent and a probe describes the binary. When a command you were confident about errors, compare csift --version with this line before anything else. Provenance labels: "measured" (with the command that ran on this surface), "docs" (a help page of this surface, a section of csift's SPEC.md at https://github.com/wdhwg001/csift/blob/v0.13.0/SPEC.md, or a Claude Code tool schema as a session of the named Claude Code version loaded it), UNTESTED (not run).
Invocation: csift <subcommand> from any directory, with csift on PATH. In commands TARGET is any form in the Targets table, P a regex pattern and PATH an absolute file path. A lane is one thread with a transcript of its own: a top-level session, or one of its subagents; on a command that spans subagents (Targets names them), @<uuid> --no-subagents keeps the session's own lane. If command -v csift prints nothing, csift is not installed: give the user cargo install --git https://github.com/wdhwg001/csift --tag v0.13.0 --locked to run (Rust 1.89 or later, docs: the README and rust-version in Cargo.toml of that repository at the v0.13.0 tag; the install UNTESTED), not crates.io, whose release can lag the tag (cargo search csift prints its version), and report what could not be read instead of parsing the jsonl.
Route by question
| you want to... | do |
|---|---|
| find where text appears, across every session | csift search 'P', then csift search 'P' TARGET to narrow; text meant literally is escaped as Search and read shows |
| see what kinds of record a scope holds before filtering | csift search '' TARGET --count-by label (only the leaves a scan without -t reads; add -t 'user.*' -t 'agent.*' -t 'harness.*' for every leaf) |
| read the person's own messages | csift search '' @<uuid> --no-subagents -t user.message; a subagent transcript holds user.message records of its own (references/lanes.md) |
| count the person's own messages | csift search '' @<uuid> --no-subagents -t user.message --count-by label counts records: typed, relayed, a mid-turn steer, a slash command with arguments; a message the conversation was later rewound past is user.rewound, which this leaves out, so add -t user.rewound to count it too (measured on one session: 676 user.message and 6 user.rewound); -c counts another unit (the unit table below) |
| read AskUserQuestion answers and typed tool rejections | csift search '' TARGET -t user.answer -t user.rejection; before listing every call that did not run, typed reason or none, read references/search.md, Recipes |
| count matches, or list the sessions that matched | csift search 'P' TARGET -c (exchange rows, not records); csift search 'P' -l |
| scope the next command to the sessions that matched, or read a field csift does not render (stop_reason, keys newer than csift) | before piping csift into csift or into jq, read references/search.md, Recipes |
| the first or the latest occurrence | csift search 'P' TARGET --max-count 1; csift search 'P' TARGET --max-count -1 (measured: --max-count -1 exits 2 on show, list and stats) |
| read one record in full, or its raw bytes | csift show TARGET --line N; csift show TARGET --line N --raw |
| the last thing the assistant said, in a session or a subagent | csift search '' TARGET --no-subagents -t agent.message --max-count -1, then csift show TARGET --line N with the last L<n> it prints; without --no-subagents the newest message can be a subagent's, and why a tail fetch does not answer this is in references/search.md, show in depth |
| a session's newest records of every kind, or its latest turns | csift show TARGET --line -30..; csift show TARGET --turn -3.., then the continue: command it prints when it stops at its cap |
| unanswered tool calls, failed tool results | csift search '' TARGET --count-by pairing; csift search '' TARGET --count-by result; without -t an answer's and a typed rejection's results sit outside both axes, while an untyped refusal counts paired and error (references/search.md, Census axes); before listing the errored or refused ones, read references/search.md, Recipes |
| which models or Claude Code versions a session ran | csift search '' TARGET --count-by model; csift search '' TARGET --count-by version |
| esc-recalled drafts, text typed into the queue | csift search '' TARGET -t user.unsent; csift search '' TARGET -t user.queued |
| claim something was never done, said or decided | before saying so, read references/search.md, Absence claims |
| what a leaf means, which leaf to pick, what a bracketed tag in a hit means | before choosing an unfamiliar leaf or reading an unfamiliar tag, search references/labels.md for it and read that row, not the whole file |
| check a label against the raw line on disk; which jsonl field decided a leaf | before judging a label from --raw bytes, search references/shapes.md for the leaf |
read search or show JSON; a search or show flag this file does not teach | before using either, read references/search.md; for one key or flag, search that file for its name |
| a range, a time window, what a turn number holds, a printed time | before writing a --line, --turn, --file-lines, --since or --until value this file does not show, comparing or reusing a turn number, or converting or comparing a printed time, read references/ranges.md |
| what a record corresponds to, what a message led to | before following provenance, read references/provenance.md |
| drafts, rewinds, forks, turns above a compaction | before reasoning about records off the conversation, read references/survival.md |
| which session is this; clones, clears, handoffs; tokens, tool calls, turn count | before listing sessions or totalling tokens, tool calls or turns, read references/sessions.md |
| files a session or its subagents changed | before listing them, read references/disk.md |
| rebuild a file or a deleted plan; a plan's approval | before running csift recover or csift plan, read references/disk.md |
| the turns a compaction summary clipped | before reconstructing them, read references/verbatim.md |
| a session's subagents, and whether one finished or is stuck; which subagent you are; what your parent asked of you; a teammate's ids, or stopping or steering one | before listing lanes, judging one, naming your own or stopping one, read references/lanes.md |
| images pasted or screenshotted into a session | before listing or extracting them, read references/image.md |
| is a session running, stopped or waiting on a person; wait on one | before judging liveness or waiting, read references/live.md; for a subagent, the row above, since status reads its parent's registry |
| message another session or subagent, or check that one arrived | before sending anything, read references/channel.md |
hand the user a hook: scripts/compact-slice.sh re-injects clipped turns after a compaction, scripts/taskstop-teammate.sh redirects a failed teammate TaskStop, scripts/elicitation-marker.sh records pending questions | before handing a user any hook, read references/hooks.md |
| an error string this file does not decode; what an exit code means | search references/errors.md for the message's first words and read that row; before a script branches on an exit code, read its Exit codes section |
Laws
- Read transcripts through csift, and never by hand-parsing the jsonl, because a hand pass is wrong without an error: a
type:"user"filter also counts tool results, peer messages and harness notifications as the person; AskUserQuestion answers and typed rejections ride inside tool_result carriers; subagent transcripts are separate files; a compaction summary replaced the words it summarised; large tool output lives intool-results/files. Instead census with--count-by label, filter with-t, and read a field csift does not render from the record's raw line (references/search.md, Recipes). - An empty result is an answer, not a syntax error: a zero-match search exits 0 and its stderr names the filters and, under a label filter, where the pattern does occur (measured:
csift search zzqq TARGET -t user.message). It is definitive only over the text the scan read: a scan without-tor--attachmentsnever reads the gated leaves (Search and read), and that stderr lists them; without--resolve-persistedit never reads the tool output Claude Code moved intotool-results/files; and no scan reads a line csift models no leaf for (references/search.md, Absence claims). Do not rewrite the syntax or fall back to hand-parsing; instead read the stderr, then reach the gated leaves with-t 'harness.*' -t user.queued,--attachmentsor the exact leaf, add--resolve-persisted, or widen the target. The one rewrite owed first is a correct literal, metacharacters escaped and a tool call's input in its JSON form (Search and read). - Every cap reports what it dropped, so a result is either whole or says it is not:
liststops at 50 rows on an unscoped run,showat 200 record units (one unit per record, or per section of a record that renders several, such as a notification carrying a result),searchandstatsare uncapped (docs:csift search --help,csift stats --help), and--max-count 0means uncapped (docs:csift --help). Never bound output with| head, which drops the closing notes and makes some commands abort (references/errors.md, Closed pipe); instead pass--max-count Ntolist,search,showorstats, the only four that take it, and bound every other command by target and filter (measured:csift <cmd> @<uuid> --max-count 3exits 2 onagents,files,image,plan,recover,status,verbatim,whoamiandmsg).| tail -1on--format jsonoutput is safe, because it reads the whole stream and keeps the summary line. Malformed lines are counted, never hidden,search -c,-land--count-byincluded: each surface's text form is in references/errors.md, Notes that are not errors, and JSON carriesskipped_lines. - Rows of
list,search,stats,files,verbatim,recover,imageandplancarry the id triosession_id/is_subagent/parent_session_id, andagentsrows carryagent_idandparent_session_idinstead (measured: each--format json). A subagent'ssession_idis its agent id (a+ 16 hex, or a teammate'sa<Name>-<16 hex>, ascsift agentsprints it): with the@sigil it addresses that subagent and any it spawned, never its session. A line number belongs to one file, so never pair it with an id taken from another record, because a parent uuid plus a subagent's line silently fetches another record; instead run the hit's ownrefetchcommand (text prints it after↳under a subagent's hit; a top-level hit needs none, since the<id8>heading its exchange is its own transcript's id cut to 8 characters and an@target itself), and scope a whole session withparent_session_id. - Before converting, comparing or subtracting a printed time, read references/ranges.md, Instants: text and JSON print an instant in different forms, and a few fields keep the harness's own wording.
Targets
Every command takes its target as a positional argument after the subcommand; search's first positional is the pattern and its targets follow it, and msg and ack name their lane with --lane @<lane> instead.
| form | scopes to |
|---|---|
@<uuid> | one top-level session (plus its subagents on the spanning commands) |
@<uuid-prefix> | the session or agent whose id starts with it: 4 to 11 dashless hex, or a dashed prefix; 12 or more dashless hex is read as an agent id, and a prefix that starts two ids is an error naming both (measured: csift list @<first N hex> --no-subagents) |
@<agent-id> | one subagent and its subtree, by its agent id (measured: 17-character ids in csift agents @<uuid> --format json) |
@<Name>@<Team> | a teammate by its routing form; an error when two teammates share it |
@main | the calling top-level session, from $CLAUDE_CODE_SESSION_ID; from a subagent that is its parent, which a stderr note says (measured: csift stats @main --no-subagents --turn -1) |
@trap:<marker> | the calling lane itself, found by a marker in the grammar references/lanes.md gives |
., a real path, or an encoded dir (-Users-dev-proj) | every session of that project |
a *.jsonl path | that transcript, and on a spanning command the subagents it spawned as well, exactly as its @ id would; that one file under --no-subagents, and always on show (measured: csift stats <session>.jsonl --format json gave sessions_in_scope 3 on a synthetic session with two subagents, 1 with --no-subagents) |
| nothing | every project under the Claude home, right for search when the question is "anywhere"; list stops at 50 rows and agents, files and stats print a block per session, so target those; plan and msg answer the calling session, verbatim exits 1, show, status and wait exit 2 (measured: each with no target) |
An id without @ is an error (did you mean '@<id>'?). list search stats files recover plan image status wait include subagent transcripts by default and take --no-subagents; verbatim spans only with --subagents; show and agents reject both flags (docs: csift --help; measured: csift show @<uuid> --line 1 --no-subagents and csift agents @<uuid> --no-subagents exit 1). --sessions-from FILE (- for stdin) adds the ids search -l prints to the targets of list search stats files recover plan image agents verbatim, each spanning exactly as its @<id> would, and an empty list scopes to nothing (docs: csift search --help; measured: csift verbatim --sessions-from - fed one uuid read 0 subagent transcripts). show takes exactly one transcript, status and wait exactly one session, and whoami refuses a session uuid (measured: csift whoami @<uuid> exits 1). Only --claude-home DIR (else $CLAUDE_CONFIG_DIR, else ~/.claude) and --cross-compactions go before the subcommand (measured: csift --format json list @<uuid> exits 2).
Search and read
search is RE2-class regex (no backreferences, no lookaround, which fail to compile) with smart case: case-insensitive unless the pattern spells an uppercase letter, as a literal, inside a bracket class or as an escape that encodes one (\x45 is E); a class escape such as \S, \W, \D or \B is no letter, and a Unicode class is folded too, so \p{Lu}rror also matches error until (?-i) turns folding off; -i forces insensitive (docs: csift search --help; measured on a synthetic transcript: \x45rror 1 record against \x65rror 2, \p{Lu}rror 2, (?-i)\p{Lu}rror 1). An empty pattern '' matches every record the filters admit.
A pattern is matched against each record's rendered text: a message, a thinking block or a tool result as its decoded text; an AskUserQuestion answer as the whole question, options and answer; a tool call as its tool name plus its input re-serialized as JSON (Bash {"command":"..."}), so inside a call's input a double quote is \", a backslash is doubled and a newline is the two characters \n, which --multiline never reaches; bracketed tags before L<line> are display only and never match, though a turn a fallback model served also prints its tag's words as its text, where they do (references/labels.md, agent.message.plain). To match text literally, put a backslash before each regex metacharacter it holds (\ . + * ? ( ) [ ] { } ^ $ |), and inside a tool call's input write the JSON form: 'commit -m \\"fix' finds the typed commit -m "fix, 'C:\\\\Users' finds C:\Users, while in message text 'C:\\Users' does (measured on a synthetic transcript: each of the three examples matched its 1 record, where 'commit -m "fix' and 'C:\\Users' under -t agent.tool.use matched 0; '\$HOME' 1 against '$HOME' 0; '[abandoned]' 10 records against '\[abandoned\]' 1). A pattern starting with - is read as flags (exit 2), so escape the dash: '\-c is' for -c is; one starting with @ is refused (references/search.md, Flags beyond the core).
Single-quote every pattern, so the shell hands it over unchanged: inside double quotes the shell turns \$ into $ before csift sees it, and a single quote inside the pattern is written '\'' or \x27, so 'it'\''s the \$HOME' finds it's the $HOME (measured on a synthetic transcript: 1 record, where "it's the \$HOME" found 0; '\$HOME' 1 against "\$HOME" 0).
-t SEL (repeatable) filters by label, -T SEL excludes. A selector is a leaf (user.message), a category (agent.tool), a role (agent) or any of those plus .*. A bare role or category narrows on three axes, keeping only the leaves the model receives, the records the harness delivered, and none the conversation was rewound or recalled past (references/labels.md, How a selector narrows); a glob or an exact leaf reaches every record. The gated leaves (harness.bookkeeping, harness.turn and harness.system leaves, user.queued, harness.schedule.event, the hook and context attachments) need a -t reaching them, which for a leaf the model never receives is the leaf or a glob, never the bare category (measured: -t harness.bookkeeping 0 records, -t 'harness.bookkeeping.*' all of them); the attachment leaves also come with --attachments, and references/labels.md gives each leaf's gate.
Three mutually exclusive terminal modes print counts instead of hits, each with its own unit:
| flag | prints | unit |
|---|---|---|
-c | one integer | exchange rows: each matched numbered turn once (eight matching tool calls in one turn count 1), plus a row per matched recalled draft or rewound opener, the message that opened a turn the conversation was later rewound past (measured: csift search '' @<uuid> --no-subagents -c printed 160 where csift stats read 138 turns and 22 abandoned ones) |
-l | owning session uuids, one per line | sessions (a subagent hit lists its parent) |
--count-by AXIS | per line a count right-aligned with leading spaces, two spaces, the key (measured: od -c); an accounting line on stderr | records; axes label tool turn session pairing model attachment version result for-kind |
--max-count N keeps the earliest N exchange rows, -N the latest N, and names the drop.
Before reading or projecting the --format json output of search or show, read references/search.md, JSON: a projection shaped after the text output reads the wrong rows and misses hits.
show TARGET fetches from exactly one transcript by --line, --uuid (the two combine) or --turn (alone), rendered in full with the leaf; with no address it refuses, so a whole transcript never lands in context by accident, and --branch-points is its one mode that takes none (references/survival.md). A --turn fetch does not return every line that carries its turn's number, show takes no -t, and its cap keeps records from the start of what was addressed: before relying on a --turn fetch to hold a whole turn, or reading show's record order, its cap or its continuation, read references/search.md, show in depth.
Text output groups hits under one heading per exchange (a matched turn, or a matched draft or rewound opener) and prints one line per hit, its leaf, L<line> and text among glyphs and tags; before reading any glyph, tag or ↳ line there, read references/search.md, Text output.
Exit codes
An exit code does not always say what a script expects of it, and a reader that closes the pipe early changes it: before a script branches on a csift exit code, read references/errors.md, Exit codes.
Commands that write, send or overwrite
csift writes to two kinds of place only: a file a recovery, reconstruction or image extraction is saved to (an --out path it replaces; a batch recover skips a file already there unless told to overwrite), and the channel files beside a session (send queues into the receiving lane's, ack and a deliver hook write the calling lane's own), where a sent, acknowledged or delivered message cannot be recalled. It never edits settings: a hook fragment, csift's or one of this skill's scripts, is merged by the user and then writes or injects on every event it serves. Each writing command, and each hook handed over, waits for the user's reply to a read-only preview saying go (the request that started the task is not that reply; a backup does not replace it). Before saving anything to disk, read references/disk.md, references/verbatim.md or references/image.md; before sending, acknowledging or delivering a message, or arming a lane, read references/channel.md; before handing the user a hook, read references/hooks.md.
Wrong assumptions
| you might assume | actually |
|---|---|
a type:"user" filter finds the person's messages | it also takes tool results, peer messages and harness notices; -t user.message with --no-subagents is the person (Laws, law 1) |
| an empty result means the syntax is wrong | a search that matches nothing succeeds, and its stderr says what was and was not read (Laws, law 2) |
| a pattern matches text as typed | a regex metacharacter needs a backslash, and a tool call's input is matched in its JSON form (Search and read) |
a flagless --count-by label lists every leaf in the scope | it counts only the leaves a scan without -t reads (Search and read) |
-c, --count-by tool and stats should agree | they count exchange rows, records and calls (Search and read, the unit table; references/sessions.md, stats) |
hits[0] is the match | a JSON row is one exchange and can hold many hits (references/search.md, JSON) |
summing .message.usage over --raw records gives a session's tokens | one response's usage repeats on each of its records; csift stats counts it once (references/sessions.md, stats) |
a subagent's session_id works as a session scope | it is the agent id and scopes that subagent; parent_session_id scopes its session (Laws, law 4) |
whoami inside a subagent names that subagent | the environment names the parent session; @trap:<marker> names you (references/lanes.md) |
| csift only reads | it writes recovered files, reconstructions, extracted images and channel messages (Commands that write, send or overwrite) |
-k means the last k | -k is the single k-th from the end; the last k is -k.. (references/ranges.md) |
| a turn begins only at a person's message, and every message the person typed begins one | answers, typed rejections, peer messages and notifications open turns too, while a relayed message or a slash command with arguments opens none (references/ranges.md, Turns) |
| a fresh random string cannot already be in your own session | your own search call records it before its result returns (references/search.md, Absence claims and the self-echo) |
| a rewound turn's edits were undone on disk | the edit reached the file; only the conversation moved past it (references/survival.md; references/disk.md) |
| a transcript lives forever | Claude Code deletes a transcript once its last write is older than the cleanupPeriodDays setting (references/sessions.md, Retention) |
レビュー
まだレビューはありません。使ってみた感想をお寄せください。