Skip to content
Back to skills

Session Audit

ASecurity

Audit Claude Code session logs (~/.claude/projects) for token waste and produce a ranked, attributed efficiency report (habit / skill_file / config). Use when the user asks to audit session efficiency, analyze token waste or cost blowups, review Claude Code usage cost, or asks "where are my tokens going".

  • 43 stars
  • 0 votes
  • 0 copies
  • 6 views
  • Added September 19, 2026
ai-agentsrustgonodegit

Works with

  • claude code
  • mcp

Security analysis

A100/100

Pro scans all 8 files and shows the line behind each finding

Scanned September 19, 2026

npx -y skills add foyzulkarim/skills --skill session-audit --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Session Audit?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Session Audit
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/foyzulkarim-session-audit/badge)](https://www.skillsdirectory.com/skills/foyzulkarim-session-audit)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: session-audit
description: Audit Claude Code session logs (~/.claude/projects) for token waste and produce a ranked, attributed efficiency report (habit / skill_file / config). Use when the user asks to audit session efficiency, analyze token waste or cost blowups, review Claude Code usage cost, or asks "where are my tokens going".
model: inherit
---

# Session-Audit

You are the reasoning layer (L2/L3) of a layered audit engine. Bundled deterministic scripts (L0/L1) digest raw session logs into metadata-only artifacts; you reason over aggregates and escalate to content only through a budget-capped fetch interface.

`{base_directory}` below refers to this skill's directory (the convention used by the runtime to inject script paths). Set the workdir once and use it everywhere:

```sh
export AUDIT_WORKDIR=<session scratchpad>/audit_workdir   # working artifacts — ephemeral
```

**Two lifetimes.** The JSON artifacts are intermediates and belong in the ephemeral workdir. The **report is the deliverable and must outlive the session** — it goes to a dated file in a stable, project-independent archive:

```sh
mkdir -p ~/.claude/audit-reports
# final report -> ~/.claude/audit-reports/<YYYY-MM-DD>.md
```

Keeping past reports is also the only way to answer "am I improving?": cross-run persistence is not implemented (I7), so trend comes from comparing today's report to the archive, not from the data layer.

## Invariants (non-negotiable)

_The list below is canonical. Each item is prefixed with its invariant id (I1–I8, with I7 intentionally omitted — cross-run persistence is not implemented; do not promise trends across runs)._

1. **(I1)** **Never read a raw session JSONL directly** (no Read/cat/grep on `~/.claude/projects/**/*.jsonl`). All access goes through `run` and `fetch`.
2. **(I2)** Reason over metadata by default; fetch content only to resolve a specific hypothesis, `--max-bytes ≤ 2000`, at most ~10 fetches per audit.
3. **(I3)** Waste math never trusts raw `input_tokens`/`output_tokens` sums (streaming placeholders / thinking excluded). Anchor on `cache_read_input_tokens`, `cache_creation_input_tokens`, and content bytes.
4. **(I4)** All aggregation is deduped by `requestId` (the scripts do this — do not hand-roll parsing).
5. **(I5)** The active session is self-excluded by the runner; never audit the session you are running in.
6. **(I6)** Report the audit's own self-cost from `fetch_log.jsonl` at the end — and label it a **lower bound**: it counts escalation fetches only, not the Phase 1 landscape or your own reasoning turns.
7. **(I8)** **Write only to `$AUDIT_WORKDIR` and the report.** Never edit this skill's own files (`SKILL.md`, `bin/`, `src/`, `data/`) or the user's repo. You are an installed bundle: a relative path like `src/views.js` points at *your own running code*, not at the project that built you. An audit that patches its own renderer measures itself with something that changed mid-run, and the next install destroys the change without a word.

## Phase 0 — Digest (L0 + L1, zero LLM)

```sh
node {base_directory}/bin/audit.js run            # full directory
node {base_directory}/bin/audit.js run --max 50   # quick pass, newest 50 sessions
```

Writes to `$AUDIT_WORKDIR`: `manifest.json` (thresholds + `pricing`: priced models, unpriced models seen, and the share of waste that carries a price), `l1_findings.json` (rule findings: `{rule, severity, sessionId, turnPointers, evidenceStats, estWasteTokens, project}`), `overview.json` (aggregates: `projects`, `tools`, `models`, `gapBuckets`, `dates`, `skills`, `sessions` sorted by waste). Per-session and per-project rollups carry `wasteUsd` alongside `wasteTokens`, plus `models`, `usdPerMTok`, and `pricedShare`.

Rules emitted: `CACHE_TTL_EXPIRY`, `DUP_TOOL_CALL`, `BIG_TOOL_OUTPUT`, `RETRY_STORM`, `CACHE_MISS_RATE`, `CONTEXT_GROWTH`, `NO_SUBAGENT`.

## Phase 1 — Read the aggregate landscape

```sh
node {base_directory}/bin/audit.js views
```

One bounded block: totals (tokens **and** dollars), findings-by-rule with a sessions-affected count, cache hit-ratio distribution, projects and worst sessions by waste (full session ids, with the `fetch` command to inspect one), idle-gap cost curve, tools by bytes, peak-context and delegation, per-date trend, skill usage.

Do **not** read `overview.json` / `l1_findings.json` wholesale (>100KB), and do not hand-roll node one-liners for anything `views` already prints — every query and its output lands in your transcript, which is the audit's real self-cost.

**If a stat you need is missing, say so in the report — do not add it to `views.js`.** You are running from an installed bundle, so `src/views.js` is *your own code*, and editing it mid-audit means the run measures itself with a renderer that changed partway through; the next install silently wipes the change anyway (I8). Record the gap under a "stats this report wanted and could not get" note, and a human ports it to the repo. Where a missing stat blocks a specific claim, a single narrow query is acceptable — but state in the report that you ran it, because the transcript cost is real and unlogged.

Two traps the view exists to prevent:

- **Finding counts are not population counts.** Every rule has entry gates — `CACHE_MISS_RATE` needs ratio < 0.5 *and* ≥5 turns — so "1 finding" never means "1 session with that property". Read the distribution block, never the rule count.
- **Raw vs. cost-equivalent tokens.** `gapBuckets.cacheCreation` is raw; rule waste is cost-equivalent (`creation × 1.15`, `rules.js:118`). The view prints the gap curve in both units. Compare like with like before claiming two methods corroborate.

## Phase 2 — L2 hypothesis loop

1. From the landscape, form candidate patterns. Recurring high-value ones: TTL expiry after idle gaps (check `gapBuckets` — cost per resume vs the `lt_1m` baseline), large-file re-read loops (DUP + BIG on `Read` in the same sessions), marathon sessions near the context ceiling, image-heavy low-cache sessions, snapshot-happy browser loops, scan-heavy sessions with `hasSubagents: false`.
2. Confirm or dismiss with narrower queries over per-session `findingsByRule` and per-session finding details.
3. Where metadata cannot resolve intent, escalate with fetch:

```sh
node {base_directory}/bin/audit.js fetch <session-id> --kind <kind> [--limit N] [--max-bytes B] [--uuid U] [--radius K]
```

| kind | returns | use to judge |
|---|---|---|
| `user_text` | user messages | intent vs agent behavior |
| `error_head` | head of errored results | transient vs deterministic failure |
| `tool_input` | tool call params | which file/command was duplicated |
| `assistant_head` | head of assistant text | verbosity / narration |
| `turn_window` | metadata around `--uuid` | sequence reconstruction |

4. Write `$AUDIT_WORKDIR/l2_hypotheses.json`:

```json
[{ "pattern": "NAME", "evidence": ["stat or fetch ref", "..."], "confidence": "high|medium|low",
   "escalation_fetches": ["..."], "resolution": "resolved|escalate", "interpretation": "one sentence" }]
```

## Phase 3 — L3 attribution

Reason over the hypotheses plus fetched snippets only. Write `$AUDIT_WORKDIR/attribution.json`, ranked by waste:

```json
[{ "rank": 1, "finding": "...", "attribution": "habit|skill_file|config",
   "estWasteShare": "~N% (xK of yK)", "trend": "improving|worsening|steady",
   "fix": "concrete behavior change, named skill edit, or config change" }]
```

Attribution guide: **habit** = working style (resuming big sessions after breaks, no /clear at pivots, inline scanning, whole-file re-reads); **skill_file** = a named skill drives the pattern (say which file and what to change); **config** = MCP bloat, model default, thinking budget, CLAUDE.md size.

## Phase 4 — Report

Write **`~/.claude/audit-reports/<YYYY-MM-DDTHHMMSSZ>.md`** (create the directory if absent). Generate the timestamp with `date -u +%Y-%m-%dT%H%M%SZ` (UTC, second-precision). No sequence suffix: the timestamp disambiguates, and a same-second collision would mean two simultaneous audits — outside this skill's pacing.

The report is read by a developer deciding **whether to spend an afternoon on this, and on what**. A finding they cannot price, verify, or check off later is a finding they will not act on. Every requirement below exists to close one of those three gaps.

**Header.** `projectsDir`, session count, and date window, so a later comparison knows its scope. Nothing else — keep tool-internal notes (artifact disagreements, re-renders, workdir paths) out of the deliverable; they read as "this tool contradicts itself" directly above the numbers. Put them in the workdir if you need them.

**Required content:**

1. **Totals in dollars *and* tokens.** `views` prints both. The dollar figure is what makes the report decidable; a token count alone is a unit nobody budgets in. Carry through both disclosures `views` prints — the unpriced-model share, and that thinking tokens and compaction are unmeasured — and label the totals a heuristic floor.
2. **Findings-by-rule table** with the **sessions-affected** column. 200 findings across 4 sessions is one bad week; across 60 it is a habit. The finding count alone cannot distinguish them, and the fix differs.
3. **Per-project breakdown** (`views` → Projects by waste). A dev acts on one repo at a time; the directory total tells them nothing about where to start. Note where the dollar and token rankings disagree — that means a pricier model, and it changes the priority.
4. **Ranked fixes**, each carrying all five of:
   - **Evidence** — real file names, real behaviors, the user's own quoted words where fetched.
   - **Full session ids**, never truncated, plus the command to check one:
     `node {base_directory}/bin/audit.js fetch <session-id> --kind user_text --limit 3 --max-bytes 500`
     An 8-char prefix cannot be resumed or fetched, so it makes the claim unverifiable.
   - **Cost** — dollars and tokens, with the share of headline.
   - **Attribution** — habit / skill_file / config.
   - **A target metric** — the exact row and number that should move by the next audit ("`CACHE_TTL_EXPIRY` 4,202K / $21 → under 1,500K"). Without one, "step away less" is unfalsifiable and next month's report has nothing to compare.
5. **A "do this first" pick, chosen on effort × permanence — not on waste rank.** A `skill_file` or `config` fix is one edit that keeps paying; a `habit` fix is indefinite discipline with no enforcement. When the top-ranked item is a habit and a smaller one is a one-line skill edit, say plainly that the skill edit goes first and why. For any `skill_file` fix, name the file and the text to change — "instruct it to read less" is not executable.
6. **Trend.** Before writing, `ls ~/.claude/audit-reports/`. If prior reports exist, read the most recent totals table and state the direction — which rules grew, which shrank, whether a previously recommended fix stuck. If the archive is empty say "first audit — no baseline yet"; do not infer a trend from within-run date buckets alone.
7. **Audit self-cost** — fetch count + bytes from `fetch_log.jsonl`, labelled a lower bound (I6).
8. **ASCII charts** — four visualizations placed **inline with the table each summarizes** in the report (findings-by-rule table → rule chart immediately after, per-project breakdown → project chart, per-date trend → date chart, cache hit-ratio section → distribution histogram). Each chart is authored in **two versions**:
   - **Wide** (~100 cols) in the report file: full labels and session counts.
   - **Compact** (~40 cols) in the chat summary: shortened labels, no session counts.
   Chart data is plotted **only from values already pulled from `views` output** (I8) — never re-derived or hand-rolled. If a stat is missing, skip that chart and note it under "stats this report wanted and could not get".

   The chat summary carries **all four compact charts** (rule, project, date, cache-hit), placed right after the headline paragraph and before the trend story, so the visual precedes the prose explanation.

### Chart format

```text
Wide version (~100 cols, label ≤ 25 chars, bar 50 chars, numbers ~20 chars):

NO_SUBAGENT       ████████████████████████████████████████████  243 sess / $73.04
CACHE_TTL_EXPIRY  ██████████████████████                        119 sess / $35.26
BIG_TOOL_OUTPUT   ████                                         62 sess /  $7.58
DUP_TOOL_CALL     ████████                                     98 sess / $14.01

Compact version (~40 cols, label ≤ 15 chars, bar 20 chars, numbers ~8 chars):

NO_SUBAGENT    ████████████████████  $73
CACHE_TTL      ██████████           $35
DUP_TOOL_CALL  ████                 $14
BIG_TOOL       ██                    $8
```

- Wrap each chart in a fenced code block so monospace alignment survives rendering.
- **Bar character**: `█` (U+2588). Round bars to whole units — no fractional blocks.
- **Layout**: right-padded label · bar · numbers. Fixed label width so bars line up.
- **Scale**: auto-scale to the maximum value so the largest bar fills the bar column.
- **Sort**: rows ordered by value descending — the worst item leads.

Then summarize the top 3 changes in chat, in the user's own context, and say where the report was saved.

## Interpretation notes

- `CONTEXT_GROWTH` carries zero direct waste — treat it as an amplifier of everything else in that session.
- `CACHE_TTL_EXPIRY` findings carry `evidenceStats.gapKind`. Only `user_idle` gaps are priced as waste; `tool_runtime` gaps are reported at **zero waste by design** — the wait was a long-running command, not a habit, and attributing it to behaviour produces a fix the user cannot act on. Check `gapKind` before calling any TTL finding a habit.
- `NO_SUBAGENT` is **excluded from headline waste** and reported in its own section. It prices a different counterfactual (delegate the phase) than the rules it overlaps (read less, read once), so summing it into the total double-counts bytes. Never add it to the headline figure.
- Rank by **prefix persistence**: early, long-riding costs beat late one-offs of the same size.
- **Dollars are priced per session, from its own model mix** — so the token ranking and the dollar ranking can legitimately disagree (a project on a pricier model costs more per wasted token). Where they do, the dollar order is the one to act on, and worth calling out. Unpriced models contribute tokens but no dollars; `manifest.pricing.pricedWasteShare` says how much of the total is covered.
- Peak context can exceed 200K on 1M-context models; don't call it a bug.
- A healthy directory (hit ratio >0.95, near-linear growth) deserves a short report saying so — do not manufacture findings.

Files in this skill

  • SKILL.md14.7 KB
  • bin/audit.js15.1 KB
  • data/model-prices.json3.4 KB
  • src/discover.js2 KB
  • src/parser.js5.2 KB
  • src/pricing.js6.1 KB
  • src/rules.js12.5 KB
  • src/views.js11 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.