Claude Code skill-invocation analytics. Local-only: parses ~/.claude/projects/**/*.jsonl transcripts directly (plus Codex, pi and opencode history with --harness) — no Enterprise admin console, no org API, no OTLP sink required. Works for any individual Claude Code install.
Rust is the primary, shipped implementation (this directory), promoted after a bake-off against Go and Python POCs (see experiments/).
Claude Code by default. --harness selects whose history to read:
--harness |
Source | What counts as a skill use |
|---|---|---|
claude (default) |
~/.claude/projects/**/*.jsonl |
every trigger type below |
codex |
~/.codex/sessions/**/*.jsonl rollouts |
direct-read: a function_call / custom_tool_call whose arguments reference skills/<name>/SKILL.md |
pi |
~/.pi/agent/sessions/*/*.jsonl |
direct-read: an assistant toolCall whose arguments reference a SKILL.md |
opencode |
~/.local/share/opencode/opencode.db |
claude-proactive: a call to opencode's native skill tool (read via the sqlite3 CLI; V1 part and V2 session_message schemas, migrated sessions counted once) |
all |
every store that exists | — |
Codex and pi have no Skill tool — an agent uses a skill by reading its file — so only tool-call
inputs are matched. Codex's model-visible skill catalog lists every installed SKILL.md path, so
grepping whole lines would count every skill in every session. Session-scoped modes (skillscope .,
skillscope <session-id>), report and fidelity stay Claude Code-only.
- Per-skill usage counts with trigger-type breakdown: user
/slash, user-named (Skill tool call the user asked for by name), model-proactiveSkilltool_use, and direct SKILL.md reads - Session-level drill-down — which sessions fired a skill, in which project/cwd
- Session-scoped modes —
skillscope .(fzf picker over sessions for the current cwd) andskillscope <session-id>(full UUID or>=8-char hex prefix) open a TUI scoped to one session, toggled between a skills-in-session table and a flat chronological timeline (Tab) - Temporal firing patterns — daily/weekly cadence, trends, dead periods
- Trigger-fidelity evals — under-triggering (skill matches intent but never fired) and
over-triggering (fired on unrelated prompts), TF-IDF weighted against each skill's frontmatter
description - Origin tracking — distinguishes invocations from main-session transcripts vs subagent
transcripts (
--origin main|subagent) - Skill inventory — join installed skills (
~/.agents/skills,~/.claude/skills, plugin marketplaces) against invocation history to see what's installed but never fires - Skill lint — check installed skills (manifest, vendored, plugin) against the reference-file
rules from Claude's skill best practices: nested references, references over 100 lines,
bundled files nothing links to, SKILL.md bodies over 500 lines (
--failfor scripts) - Reference reads — per bundled file (
references/*.md,scripts/*): how deep it sits below SKILL.md, its size and TOC, and whether transcripts read it in full, in part (head -100, Readlimit) or only searched it, by which model; files of a used skill that are never read
A Rust toolchain (edition 2021, stable). If you already manage Rust with rustup or your distro's packages, use that — skillscope has no special toolchain requirements.
If you have no existing preference, asdf with the asdf-rust plugin keeps the toolchain pinned per-project:
asdf plugin add rust https://github.com/asdf-community/asdf-rust.git
asdf install rust latest
asdf set rust latest # writes .tool-versions in the repobrew install plinde/tap-private/skillscope # release build (macOS arm64, Linux x86_64)
make build # dev build: target/release/skillscope
skillscope # TUI: skills -> sessions -> invocations drill-down
skillscope . # fzf picker over sessions for the current cwd -> scoped TUI
skillscope <session-id> # scoped TUI for one session (full UUID or >=8-char hex prefix)
skillscope summary # per-skill counts + trigger breakdown
skillscope sessions <skill> # session drill-down for one skill
skillscope timeline [skill] # time-series (daily/weekly)
skillscope projects # per-project breakdown
skillscope fidelity # trigger-fidelity report
skillscope report [skill] # per-cwd session survey with trigger context
skillscope inventory [skill] # installed-skill inventory joined against invocation history
skillscope lint [skill] # reference-file rules for installed skills (--fail: exit 1 on findings)
skillscope refs [skill] # per bundled file: depth, lines, TOC, full/partial/search reads (--by-model)
skillscope refs <skill> --reads # the matched tool calls as JSON lines
skillscope export # JSON export of normalized invocations
skillscope summary --harness all # include Codex, pi and opencode history (default: claude)Every merge to main releases. .github/workflows/release.yml picks the next version (minor
bump if any feat commit since the last v* tag, else patch; docs-only merges skip; a PR that
already bumped Cargo.toml past the last tag sets the version), commits the Cargo.toml /
Cargo.lock bump to main as chore(release): vX.Y.Z, tags it, then builds, all on ubuntu-latest, aarch64-apple-darwin (cross-built with cargo-zigbuild, ad-hoc signed by zig's linker) and x86_64-unknown-linux-musl (static,
any glibc), publishes a GitHub Release, and pushes Formula/skillscope.rb to
plinde/homebrew-tap-private (needs the TAP_GITHUB_TOKEN secret); brew upgrade picks it up.
PRs that touch the build inputs run the builds only. workflow_dispatch releases if main has
unreleased changes, else builds only. A manual v* tag that matches Cargo.toml still releases
exactly that tag.
-
User slash invocation (
user-slash) —type:"user"line,message.contentis a string containing<command-name>/foo</command-name>(plus<command-message>,<command-args>). -
Skill tool call —
type:"assistant"line,message.content[]entry withtype:"tool_use",name:"Skill",input.skill= skill name, optionalinput.args. Split by the most recent real user prose before it:user-named— that prose names the skill as/fooor$foo(case-insensitive; for a plugin skillplugin:foo,/foocounts too). The user asked for it; the model ran it.claude-proactive— nothing asked for it; the description/keyword trigger fired.
"Real user prose" is the text of a
type:"user"line minus<system-reminder>blocks, and skips tool results,isMetalines, skill bodies (Base directory for this skill…), interrupt markers, and anything carrying<command-name>,<task-notification>,<cross-session-message, or local-command output. A typed/commandline counts as the user's latest input. Mentions must be whole tokens:~/x/foo,/foo-bar,/foo.mddon't namefoo. -
Direct read (
direct-read) — any other assistanttool_use(Read, Bash, Grep, …) whose input containsskills/<name>/SKILL.md. One record per skill per message (Claude Code writes a message's content blocks as separate lines sharingmessage.id). Writes/edits and delegations (Agent,Task,SendMessage) are not reads. -
Subagent transcripts —
<project>/<session-uuid>/subagents/agent-*.jsonl, same schema, tagged withorigin: subagent. -
Reference reads (
skillscope refs, a separate pass: invocation counts don't change) — any tool call namingskills/<name>/<relpath>with relpath notSKILL.md, in Claude, Codex and pi transcripts. The extent comes from the tool's own fields:- full — Read with no
limit/offset;cat,nl;sed -n '1,$p'. - partial — Read
limit(oroffset);head/tail(-N,-n N; default 10);sed -n 'a,bp';awk 'NR<=N'. Acat/nlpiped into one of these takes its bound. - search —
grep/rg, the Grep tool. - unknown — anything else naming the file (running a script,
less/more).
Shell commands are split at
|,&&,;and only the segment naming the file decides. Commands that don't read text (ls,wc,git,sed -i) and tools that only quote paths (plans, todos) are skipped. The model is the assistantmessage.model(Claude, pi) or the Codexturn_context.model. One record per file per message. - full — Read with no
-
sessions-index.json— Claude Code's own per-project session index (firstPrompt/summary/gitBranch/modified/projectPath), joined for friendlier session labels and recency sorting.
Each transcript line also carries sessionId, cwd, timestamp (ISO 8601). Every exported
record carries harness (claude, codex, pi, opencode).
From Claude's skill authoring best practices:
Claude may preview nested reference files with head -100 and so read them only in part.
evals/, agents/openai.yaml (Codex metadata), LICENSE* and dot-dirs are not bundled content.
Claude Code deletes local session transcripts under ~/.claude/projects/ after 30 days by
default — this is the cleanupPeriodDays setting, and the cleanup sweep runs at every startup.
See Data usage: Data retention and
Settings: cleanupPeriodDays in the
official docs.
This caps how far back skillscope timeline, report, and fidelity can meaningfully look —
anything older than the retention window is already gone from disk by the time you run the tool,
regardless of skillscope's own logic. A month is thin for spotting slow-moving trends (a skill
that fires quarterly, a fidelity regression that crept in over months). If you want a longer
retrospective window, raise the limit before you need the history, since it only protects
transcripts going forward:
// ~/.claude/settings.json
{
"cleanupPeriodDays": 180
}0 is invalid (fails validation) and is not "keep forever" — use a large value instead (e.g.
9999) if you want retention to be effectively indefinite. Raising this trades disk space
(transcripts are plaintext JSONL, and can be sizeable on an active install) for analytics depth;
there's no partial option — it's a single global setting, not "keep only skillscope-relevant
sessions."
src/
├── models.rs # SkillInvocation, RefRead, ReadExtent, Origin, TriggerType, Harness
├── parser.rs # JSONL streaming extraction (main + subagent + scoped)
├── harness.rs # Codex, pi and opencode session stores
├── aggregate.rs # counts, trigger breakdown, time-series, per-project rollups
├── sessions.rs # sessions-index.json join
├── sessionscan.rs # cwd -> session discovery for the `.` picker
├── resolve.rs # session-id / hex-prefix resolution
├── fzf.rs # fzf picker wrapper (line build/parse, process I/O)
├── fidelity.rs # skill discovery + TF-IDF trigger-fidelity heuristics
├── report.rs # per-cwd session survey
├── inventory.rs # installed-skill inventory join
├── skilltree.rs # skill dir walk: bundled files, links, depth, lint rules
├── refreads.rs # bundled-file reads + extent (full/partial/search) per tool call
├── cli.rs # clap subcommands + global flags
└── tui/ # ratatui: global + session-scoped drill-down views
SKILLSCOPE_TFIDF_THRESHOLD (default 20.0) and SKILLSCOPE_MIN_SESSION_COUNT (default 8) are
env-overridable to tune the trigger-fidelity heuristic without a code change.
An Agent Skill ships in-repo at
.agents/skills/skillscope/ so a coding agent can answer
skill-usage questions on request ("what skills did session abc12345 invoke, when, and how?")
by driving skillscope export --json | jq non-interactively. It's picked up automatically by
agents that read .agents/skills/ at project scope; for global availability, symlink or copy it
into ~/.agents/skills/.
Two earlier bake-off entries are kept as reference/experiments, not shipped:
experiments/python/— the original Python POC; still the parity oraclemake paritychecks the Rustexportoutput againstexperiments/go/— a Go + Bubbletea rewrite from the same bake-off
