npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@uluops/agent-metrics

v0.12.1

Published

Extract accurate metrics from Claude Code and Codex agent session files

Readme

UluOps · The operations layer for agentic work


Agent Metrics

Extract accurate, normalized metrics from agent session files across coding harnesses.

Overview

Coding harnesses record execution data in their own on-disk formats. This utility reads those formats and normalizes token counts, timing, and execution statistics into a shared shape. Each record carries a harness field identifying its origin; the available observations and counter meanings still depend on the harness.

Supported harnesses:

  • Claude Code — agent (Task tool) session files under ~/.claude/projects/
  • Codex — subagent session rollouts under ~/.codex/sessions/ (or $CODEX_HOME/sessions/)
  • Planned — Gemini CLI and OpenCode. The extractor architecture is harness-agnostic (a per-harness reader feeding a shared normalizer), so adding a harness does not change the public output shape.

Harness selection is per-command via --provider auto|claude|codex (default auto detects the harness from the agent ID). The normalized output and the tracker wire format are identical across harnesses; harness-specific token components (e.g. Codex cached_input, reasoning_output) are carried as additive optional fields.

Current measurement limits

Codex extraction reads a cumulative session snapshot. Reusing an agent for a follow-up includes earlier work in its totals; repeated captures are not separate executions and should not be summed. Turn-scoped execution IDs and capture deduplication remain proposed work, not features of this release.

Missing Codex token usage still falls back to zero in the current output contract. A zero therefore does not prove that no tokens were used. Malformed later usage records retain the last valid cumulative totals. Tool counts describe recorded calls, and Codex error counts recognize structured failure results; they are not a guarantee that every failed operation was observed.

Prerequisites

  • Node.js 18+ — Required for ESM support
  • At least one supported harness:
    • Claude Code — agent session files in ~/.claude/projects/
    • Codex — optional; subagent rollouts in ~/.codex/sessions/ or $CODEX_HOME/sessions/

Installation

Via npm (Recommended)

npm install -g @uluops/agent-metrics

The agent-metrics command is now available globally:

agent-metrics --version
agent-metrics list

Via UluOps Setup

If you use @uluops/setup, agent-metrics is installed automatically with the SubagentStop hook pre-configured:

npx @uluops/setup

From Source

git clone https://github.com/Uluops/-uluops-agent-metrics.git
cd -uluops-agent-metrics
npm install
npm run build
npm link

To refresh the SubagentStop hook from a source build, run ./install.sh — the hook is wired to a persistent copy at ~/.claude/tools/agent-metrics/, not to the global npm install, so npm link alone does not change what the hook runs.

Development checks

npm run lint                  # tsc --noEmit
npm test                      # node:test suite (compiled via tsconfig.test.json)
npm run check:pack            # the packed tarball contains no test artifacts (--control proves it can fail)
npm run check:readme-exports  # README import blocks match src/index.ts exports (--control proves it can fail)

prepublishOnly runs npm test && npm run build && npm run check:pack in that order: the test compile writes into dist/, so build (which cleans first) must come last for the tarball to contain only the production emit.

As a Project Dependency

npm install @uluops/agent-metrics
import { extractAgentMetrics, queryBuffer } from '@uluops/agent-metrics';

Uninstall

npm uninstall -g @uluops/agent-metrics

Usage

Quick Start

# List recent Claude Code agent session files
agent-metrics list

# List recent Codex subagent rollouts
agent-metrics list --provider codex

# Extract metrics for a run from the list
agent-metrics extract a80e24f -f summary

# Show buffered hook captures after SubagentStop auto-capture is configured
agent-metrics report --current

# Show buffer statistics (alias for buffer status)
agent-metrics status

# Show usage examples for common workflows
agent-metrics examples

Output of agent-metrics list (one line per agent file found on disk: id, duration, tokens, tool count, project):

Recent Agent Runs
════════════════════════════════════════════════════════════════════════════════

a559bfdfa3bae750a  │  6m 58s    │  430.4k   │  57 tools  │  ops-uluops-api
aaf54a8ba5e6bf3be  │  3m 35s    │  181.9k   │  43 tools  │  ops-uluops-api

Use `agent-metrics extract <agent-id>` for detailed metrics

Agent IDs must be passed in full as list prints them (17-18 hex characters); there is no prefix matching, and a shortened id fails with "Agent file not found".

Output of agent-metrics report --current (buffered hook captures, with agent name, model and cache rate, grouped by project):

Recent Agent Metrics
══════════════════════════════════════════════════════════════════════════════════════════════════════════════

Agent ID            │  Agent Name              │  Model      │  Duration  │  Tokens   │  Cache  │  Tools
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
  ┌ ops-uluops-api (3 agents, 17m total, 585k tokens)
  │ a77373c5debd07e95  │  type-safety-validator    │  sonnet-4-6  │   3m 9s    │  155k     │   93%  │     44
  │ a02cd0561f2543ed0  │  test-architect           │  sonnet-4-6  │   6m 4s    │  268k     │   93%  │     46
  │ aa926931b1d22ba2b  │  public-interface          │  sonnet-4-6  │   8m 42s   │  162k     │   95%  │     58
  └
a5511f269fb53e254    │  code-auditor              │  opus-4-6    │   3m 2s    │  466k     │   88%  │     46

List and report columns:

  • Agent Name — auto-detected from [agent:name] tags; falls back to project name
  • Cache% — cache hit rate (cache_read / (cache_read + cache_creation + input) * 100)
  • Agents from the same conversation turn are grouped with a summary header

Extract Metrics

# Single agent (JSON)
agent-metrics extract a80e24f
agent-metrics extract a80e24f --json        # --json alias for -f json
agent-metrics extract a80e24f -f summary    # Human-readable

# Codex subagent by UUIDv7 id
agent-metrics extract 019eaa28-8e2d-73a2-840f-a00d6cc8795f --provider codex

# Multiple agents (batch) — outputs JSON array
agent-metrics extract a7c642b a03c37d af0c1a1

# Tracker-ready format (for mcp__uluops-tracker__update_run).
# `buffer list -f tracker` re-extracts each entry from its transcript when the transcript
# still exists (v0.12.0): the hook's capture can miss the agent's final message (74629040).
agent-metrics extract a7c642b -f tracker --agent-name code-validator

# Named extraction without buffer writes (useful in read-only sandboxes)
agent-metrics extract a7c642b -f tracker --agent-name code-validator --no-annotate-buffer

# Batch tracker format with named mapping
agent-metrics extract a7c642b a03c37d af0c1a1 \
    -f tracker \
    --agent-names "code-validator,test-architect,security-analyst"

# Scope the search to one project path
agent-metrics extract a80e24f -p ~/uluops/ops-uluops-api

Named extraction normally annotates matching buffer entries with the supplied name. --no-annotate-buffer skips that write and its lock acquisition while preserving the name in the output. This works for single and batch extraction.

Compare Multiple Agents

agent-metrics compare a5b1804 ac51171 a0a96d3 a80e24f

Find Agent File Location

agent-metrics find a80e24f
agent-metrics find 019eaa28-8e2d-73a2-840f-a00d6cc8795f --provider codex

List Recent Agent Runs (from disk)

agent-metrics list
agent-metrics list -n 20
agent-metrics list --provider codex

Provider defaults to auto: UUIDv7 agent ids route to Codex, and Claude-style hex ids route to Claude. report remains Claude-buffer-backed in this release; use list --provider codex and extract <id> --provider codex for Codex runs.

Output Formats

JSON (default)

Complete metrics object:

{
  "harness": "claude-code",
  "agent_id": "a80e24f",
  "session_id": "ea588859-88cd-4511-851a-4fe928cd77c7",
  "slug": "humble-forging-pony",
  "model": "claude-sonnet-4-5-20250929",
  "git_branch": "main",
  "cwd": "/home/user/project",
  "claude_code_version": "2.0.76",
  "prompt_id": "31853822-649f-45aa-a7ae-0027726335c5",
  "start_time": "2026-01-07T20:07:03.733Z",
  "end_time": "2026-01-07T20:08:56.859Z",
  "duration_ms": 113126,
  "duration_formatted": "1m 53s",
  "tokens": {
    "input": 667,
    "output": 1510,
    "cache_creation": 63236,
    "cache_read": 315202,
    "total_effective": 65413,
    "total_raw": 380615
  },
  "execution": {
    "message_count": 30,
    "tool_use_count": 13,
    "tool_breakdown": { "Bash": 12, "Read": 1 },
    "error_count": 0
  }
}

Codex metrics use the same top-level shape with harness: "codex" and Codex-specific additive fields such as codex_cli_version, parent_thread_id, tokens.cached_input, tokens.reasoning_output, and execution.reasoning_record_count.

v0.6.0: the top-level metrics field was renamed provider → harness (values claude → claude-code; codex unchanged). The --provider CLI option is the unrelated dispatch selector and is unchanged. tokens also gained optional thinking / tool components (subsets of gross output, populated by future providers), and the record carries harness through to the tracker wire.

Summary Format

Agent Metrics: a80e24f
══════════════════════════════════════════════════════

┌─ Identification
│  Agent ID:    a80e24f
│  Session:     ea588859-88c...
│  Slug:        humble-forging-pony

┌─ Context
│  Model:       claude-sonnet-4-5-20250929
│  Branch:      main
│  Version:     2.0.76

┌─ Timing
│  Start:       2026-01-07T20:07:03.733Z
│  End:         2026-01-07T20:08:56.859Z
│  Duration:    1m 53s (113,126ms)

┌─ Tokens
│  Input:       667
│  Output:      1,510
│  Cache Create:63,236
│  Cache Read:  315,202
│  ─────────────
│  Effective:   65,413 (excl. cache reads)
│  Raw Total:   380,615

┌─ Execution
│  Messages:    30
│  Tool Uses:   13
│  Errors:      0

┌─ Tool Breakdown
│  Bash: 12
│  Read: 1

Tracker Format

Ready for save_run:

{
  "name": "prompt-quality-validator",
  "agent_id": "a80e24f1c2d3e4f5a",
  "model": "claude-sonnet-4-5-20250929",
  "tokens": {
    "input_tokens": 63903,
    "output_tokens": 1510,
    "cache_creation_tokens": 63236,
    "cache_read_tokens": 315202,
    "total_effective_tokens": 65413
  },
  "duration_ms": 113126,
  "definition_version": "1.4.0"
}

agent_id (v0.7.0) is the transcript/agent provenance id — it makes the saved tracker row joinable back to its buffer entry and session transcript.

definition_version (v0.12.0) is the version of the agent definition that ran, captured by the hooks (see Definition capture). Splice it with the rest of the entry: the tracker credits a run to an agent version only when the caller sends that agent's own version, and stores a server-filled inferred-latest (counted toward no version) when it is absent. It is omitted whenever the definition could not be named — never guessed — whenever the entry is reported under a different name than the definition's own, and when the label is longer than 50 characters (save_run rejects the whole call on an over-long value, and a truncated label would name a version that does not exist). The content hash and path stay in the buffer: save_run's agents[] schema is strict and has no field for them.

Token Calculations

| Metric | Claude calculation | Codex calculation | Use Case | |--------|--------------------|-------------------|----------| | total_effective | input + cache_creation + output | (input - cached_input) + output | Provider-local tracker parity | | total_raw | All tokens summed | Codex-reported total_tokens | True total processed |

Note: Cache reads are excluded from Claude total_effective because they're significantly cheaper than other token types. Codex cached input is likewise subtracted (the analog of Claude's excluded cache read). reasoning_output (and thinking/tool) are subsets of gross output — stored for cost breakdown but never added to total_effective (v0.6.0 removed the prior + reasoning_output double-count). OpenAI cached input is discounted rather than free, so cross-provider cost rollups should re-price from raw provider token fields.

Edge cases:

  • When cache_creation is 0 (no new context cached), total_effective equals input + output
  • When input is 0 (all input served from cache), total_effective equals cache_creation + output
  • For very short agents (single turn), cache_read is typically 0 since there's no prior context to read from cache

Programmatic Usage

npm install @uluops/agent-metrics

Core Extraction Functions

import {
  extractAgentMetrics,
  extractMetricsFromFile,
  extractMultipleAgentMetrics,
  formatMetricsSummary,
  toTrackerFormat,
} from '@uluops/agent-metrics';

async function main() {
  // Extract by agent ID (searches all projects)
  const metrics = await extractAgentMetrics('a80e24f');
  if (!metrics) {
    throw new Error('Agent metrics not found');
  }

  console.log(`Duration: ${metrics.duration_formatted}`);
  console.log(`Tokens: ${metrics.tokens.total_effective}`);

  // Extract a Codex subagent by UUIDv7 id
  const codexMetrics = await extractAgentMetrics(
    '019eaa28-8e2d-73a2-840f-a00d6cc8795f',
    { provider: 'codex' }
  );

  // Auto mode routes UUIDv7 ids to Codex and Claude-style hex ids to Claude
  const autoMetrics = await extractAgentMetrics('019eaa28-8e2d-73a2-840f-a00d6cc8795f');

  // Extract from a specific file path
  const metricsFromFile = await extractMetricsFromFile('/path/to/agent-abc123.jsonl');

  // Extract multiple agents at once
  const multipleMetrics = await extractMultipleAgentMetrics(['a80e24f', 'b91f35g']);

  // Format as human-readable summary
  const summary = formatMetricsSummary(metrics);
  console.log(summary);

  // Convert to validation tracker format
  const trackerData = toTrackerFormat(metrics, 'code-validator');
}

main().catch((error) => {
  console.error(error);
});

Error signalling

These functions distinguish "not found" from "found but unusable" differently — check which contract applies before assuming a null/empty result means the same thing as a missing throw:

  • extractAgentMetrics — can both return null AND throw. It returns null only when no matching agent file could be located at all. If a file WAS found but could not be read or contained no valid messages/records, it throws (Claude path via extractMetricsFromFile; Codex path via extractCodexMetricsFromFile). The example above only shows the null branch — do not assume a bare if (!metrics) ... check is sufficient; wrap the call, or let the throw propagate, if the file-found-but-corrupt case matters to the caller.
  • extractMultipleAgentMetrics — never rejects the batch. Each id resolves to its metrics or to null (not-found OR extraction-failed, collapsed); a failure is written to stderr naming the id and the reason.
  • extractMetricsFromFile — throws (never returns null) if the path cannot be read, or if the file was readable but contained no valid messages.
  • findAgentFile — returns null if no matching file exists; does not throw for not-found.
  • appendToBuffer — never appends to the buffer without its lock. When the lock cannot be acquired (after the 5s backoff), the entry is written to its own file in <buffer>.spill/ and returned (v0.12.0); readBuffer includes spilled entries, and the next locked append — or any rewrite (clear*, cleanupExpired, annotateBufferEntries) — drains them into the buffer first, so a clear removes spilled entries too. It returns null (not a throw) only when the spill itself fails. Before v0.12.0 a contended entry was skipped and lost.
  • cleanupExpired, clearSession, clearAgents, annotateBufferEntries — throw LockAcquisitionError on lock contention (unlike appendToBuffer, these are explicit maintenance/query operations, not a fire-and-forget hook capture).
  • readBuffer — returns [] if the buffer file does not exist yet; throws if the file exists but cannot be read; silently skips (not throws) individual lines that fail to parse or fail shape validation. Collapses to one entry per agent_id (v0.12.0): a re-woken agent stops more than once, and only the latest capture (by end_time, then captured_at) is returned — so readBuffer().length counts agent instances, not lines. The rewrite operations still act on every raw line: clearAgents and annotateBufferEntries touch all of an agent's lines.

Buffer Functions

import {
  queryBuffer,
  getAllForSession,
  getLatestForSession,
  getBufferStats,
  appendToBuffer,
  clearSession,
  clearAgents,
  cleanupExpired,
  annotateBufferEntries,
  readBuffer,
  readValidEntries,
  entriesToTrackerFormat,
  LockAcquisitionError, // thrown by the four rewrite ops under lock contention
} from '@uluops/agent-metrics';

// Query buffer with filters
const entries = queryBuffer({
  sessionId: 'ea588859-88cd-4511-851a-4fe928cd77c7',
  agentName: 'code-validator',
  runId: 'my-project-ship-31948701-01', // exact run token — only agents tagged [run:...] with it (v0.8.0)
  since: new Date(Date.now() - 3600000), // Last hour
  includeExpired: false,
});

// LockAcquisitionError exposes only .message/.name — catch it to retry or skip
try {
  cleanupExpired();
} catch (e) {
  if (e instanceof LockAcquisitionError) {
    // skip or retry
  } else {
    throw e;
  }
}

// Append with a run token so a later queryBuffer({ runId }) / `buffer list --run`
// collects exactly this run's agents (v0.8.0). run_id is a query key only —
// it is NOT emitted into -f tracker output (the tracker schema is strict).
appendToBuffer(metrics, {
  agentName: 'code-validator',
  runId: 'my-project-ship-31948701-01',
});

// Get all entries for a session (useful for workflows)
const sessionEntries = getAllForSession('ea588859-...');

// Get most recent entry for a session
const latest = getLatestForSession('ea588859-...');

// Get buffer statistics
const stats = getBufferStats();
console.log(`Total entries: ${stats.totalEntries}`);
console.log(`Valid entries: ${stats.validEntries}`);

// One entry per agent instance (including expired); spill files included
const allEntries = readBuffer();

// Read only valid (non-expired) entries
const validEntries = readValidEntries();

// Convert entries to tracker-compatible format
const trackerData = entriesToTrackerFormat(validEntries);

// Cleanup operations
const expiredCount = cleanupExpired();
const clearedCount = clearSession('ea588859-...');

Utility Functions

import {
  findAgentFile,
  findRecentAgentFiles,
  findCodexAgentFile,
  findRecentCodexAgentFiles,
  getClaudeProjectsDir,
  getCodexSessionsDir,
  sanitizePathAsFolderName,
  getProjectName,
  extractAgentIdFromFilename,
  extractCodexAgentIdFromFilename,
  parseTimestamp,
  calculateDuration,
  formatDuration,
  formatTokens,
  formatNumber,
  formatModelName,
} from '@uluops/agent-metrics';

// Find agent file location (searches flat and session/subagents layouts)
const location = findAgentFile('a80e24f');
if (location) {
  console.log(`Path: ${location.filePath}`);
  console.log(`Project: ${location.projectDir}`);
}

// Find recent agent files across all projects
const recentFiles = await findRecentAgentFiles(20); // Last 20
const recentCodexFiles = await findRecentCodexAgentFiles(20);

// Find Codex rollout file location
const codexLocation = await findCodexAgentFile('019eaa28-8e2d-73a2-840f-a00d6cc8795f');

// Path utilities
const projectsDir = getClaudeProjectsDir();           // ~/.claude/projects
const sessionsDir = getCodexSessionsDir();            // ~/.codex/sessions or $CODEX_HOME/sessions
const folder = sanitizePathAsFolderName('/Users/me/myproject'); // "-Users-me-myproject"
const name = getProjectName('/path/to/-Users-me-myproject');    // "myproject"

// Agent ID extraction from filenames
const id = extractAgentIdFromFilename('agent-a80e24f.jsonl'); // "a80e24f"
const codexId = extractCodexAgentIdFromFilename(
  'rollout-2026-06-27T03-00-00-000Z-019eaa28-8e2d-73a2-840f-a00d6cc8795f.jsonl'
);

// Timestamp utilities
const date = parseTimestamp('2026-03-29T06:21:53.455Z'); // Date object
const durationMs = calculateDuration(
  '2026-03-29T06:21:53.455Z',
  '2026-03-29T06:25:12.000Z'
); // 198545

// Formatting utilities
console.log(formatDuration(113126));   // "1m 53s"
console.log(formatTokens(65413));      // "65.4k"
console.log(formatNumber(1234567));    // "1,234,567"
console.log(formatModelName('claude-sonnet-4-5-20250929', 12)); // "sonnet-4-5"

Logger Functions

configureLogger validates its input and never throws: an undefined value, an unknown minLevel, a maxFiles that is not an integer ≥ 1, or a maxFileSize that is not a finite number > 0 is rejected with a stderr warning naming the key and value, and the current setting is retained (never reset to the default). Sibling keys in the same call still apply.

import {
  configureLogger,
  getLoggerConfig,
  readRecentLogs,
  getLogStats,
  logDebug,
  logInfo,
  logWarn,
  logError,
  logMetricsCapture,
  logBufferOperation,
} from '@uluops/agent-metrics';

// Configure logger
configureLogger({
  enabled: true,
  minLevel: 'info',
  logPath: '/custom/path/metrics.log',
});

// Get current config
const config = getLoggerConfig();

// Read recent log entries
const lines = readRecentLogs(50);

// Get log file stats
const logStats = getLogStats();
console.log(`File size: ${logStats.sizeBytes}`);
console.log(`Line count: ${logStats.lineCount}`);

// Log messages (if logging enabled)
logInfo('Processing agent metrics');
logDebug('Detailed debug info');
logWarn('Warning message');
logError('Error occurred');

// Structured logging for metrics operations
logMetricsCapture(
  'agent-abc123',
  'session-xyz',
  {
    model: 'claude-sonnet-4-5',
    duration_ms: 5000,
    tokens: {
      input: 1000,
      output: 250,
      cache_creation: 0,
      cache_read: 0,
      total_effective: 1250,
      total_raw: 1250,
    },
    execution: {
      message_count: 4,
      tool_use_count: 1,
      tool_breakdown: { Read: 1 },
      error_count: 0,
    },
  },
  { agentName: 'code-validator', source: 'hook' }
);
logBufferOperation('append', { agent_id: 'abc123', buffer_path: '/path/to/buffer' });

Types

import type {
  // Core metrics types
  AgentMetrics,
  TokenMetrics,
  ExecutionMetrics,
  ExtractOptions,
  MetricsProvider,
  AgentFileLocation,
  // Tracker format types
  TrackerTokens,
  TrackerFormat,
  TrackerAgentFormat,
  // Buffer types
  BufferEntry,
  BufferConfig,
  BufferStats,
  AppendOptions,
  BufferQuery,
  // Format types
  ExtractFormat,
  BufferFormat,
  // Logger types
  LogLevel,
  LoggerConfig,
  LogStats,
} from '@uluops/agent-metrics';

Data Source

Agent session files are stored at:

~/.claude/projects/{project-folder}/{session-uuid}/subagents/agent-{id}.jsonl

Codex subagent rollout files are stored at:

~/.codex/sessions/YYYY/MM/DD/rollout-{timestamp}-{uuidv7}.jsonl

Each JSONL file contains all messages from an agent invocation, including:

  • User prompts
  • Assistant responses with token usage
  • Tool invocations and results
  • Timestamps for each message

Commands Reference

Run agent-metrics examples for a built-in usage guide covering common workflows.

Core Commands

| Command | Description | |---------|-------------| | status | Show buffer statistics (alias for buffer status) | | report [-n limit] [-s session] [--current] [--provider auto|claude|codex] | Show recent Claude-buffer auto-captured metrics; codex exits with guidance because report is buffer-backed | | list [-n <limit>] [-p project] [--provider auto|claude|codex] | List recent agent runs from session files | | extract <ids...> [-p project] [-f format] [--json] [-a agent-name] [--agent-names names] [--no-annotate-buffer] [--provider auto|claude|codex] | Extract metrics for one or more agents; optionally skip buffer name write-back and locking | | compare <id...> [-p project] [--provider auto|claude|codex] | Compare multiple agents side-by-side (auto resolves each id's harness independently, so a mixed Claude+Codex comparison works) | | find <id> [-p project] [--provider auto|claude|codex] | Find the file location for an agent | | reconcile --run <token> --expect <n> [-p project] [-f format] [-a] (v0.10.0) | Verify every agent expected in a --run <token> was attributed; moves ADR-0004's count-check into this artifact. Exit codes: 0 = attributed matches or exceeds expected (over-collection is benign, reported on stderr); 1 = shortfall (attributed < expected — likely a dropped [run:] tag); 2 = usage error (missing/malformed --run or --expect), deliberately distinct from 1 so a mistyped flag can't read as a passing check | | examples | Show usage examples for common workflows |

Buffer Commands

| Command | Description | |---------|-------------| | buffer status | Show buffer statistics | | buffer list [-s session] [--agent-name name] [--run token] [--since duration] [--end-after iso] [--end-before iso] [-p project] [-a] [-f format] | List buffered entries. --run <token> filters to a single orchestrator run token (exact match, v0.8.0) | | buffer session <id> [-f format] | Get all entries for a session | | buffer clear --session <id> | Clear entries for a session | | buffer clear --agents <id...> | Clear specific agent IDs | | buffer clear --expired | Clear only expired entries (garbage collect) |

Log Commands

| Command | Description | |---------|-------------| | log status | Show log file statistics | | log tail [-n lines] [-f] | View recent log entries (supports follow mode) | | log clear [--all] | Clear log file (--all removes rotated files) | | log path | Print log file path (useful for scripting) |

Log Management

The hook logs all operations for debugging and auditing.

View Log Status

agent-metrics log status

Output:

Agent Metrics Log Status
══════════════════════════════════════════════════
Log file:          /home/user/.claude/agent-metrics.log
Logging enabled:   true
Min level:         info
Max file size:     5.0 MB
Max rotated files: 3

File exists:       true
File size:         12.4 KB
Line count:        156
Rotated files:     0

View Recent Logs

# Show last 20 lines
agent-metrics log tail

# Show last 50 lines
agent-metrics log tail -n 50

# Follow log in real-time (like tail -f)
agent-metrics log tail -f

Clear Logs

# Clear current log file
agent-metrics log clear

# Clear all logs including rotated files
agent-metrics log clear --all

Log Path

# Print log file path (useful for scripting)
agent-metrics log path

Auto-Capture with SubagentStop Hook

The agent-metrics hook automatically captures metrics when any Task tool agent completes. This enables accurate token tracking across validation workflows.

How It Works

  1. When a Task tool agent finishes, Claude Code fires a SubagentStop hook
  2. The hook receives the agent's transcript path (~/.claude/projects/.../agent-{id}.jsonl)
  3. agent-metrics extracts tokens, timing, and execution stats from the transcript
  4. Metrics are written to a global buffer (~/.claude/agent-metrics-buffer.jsonl)
  5. Buffer entries expire after 30 days (configurable), aligned with Claude Code transcript retention; expired entries are garbage-collected opportunistically on the next append (v0.7.0 — expiry is enforced, not a passive label)
  6. Buffer lines the reader cannot validate (malformed JSON, or entries missing a field every consumer dereferences) are skipped with a stderr warning naming the missing field, and are preserved verbatim across GC and other rewrites rather than deleted (v0.9.0). The hook itself never fails the parent session: stdin is read under a hard deadline and a 1MB cap, and every capture-path failure is logged rather than thrown.

BREAKING (v0.7.0): buffer-rewrite operations are fail-closed. When the buffer lock cannot be acquired, cleanupExpired, clearSession, clearAgents, and annotateBufferEntries now throw LockAcquisitionError (exported) instead of proceeding unlocked — an unlocked read-modify-rewrite could silently destroy concurrently-captured entries. Catch it to retry or skip: catch (e) { if (e instanceof LockAcquisitionError) ... }. The buffer clear CLI reports a clean locked-buffer message; hook capture (appendToBuffer) is unchanged — it was already fail-closed.

Setup

@uluops/setup configures this hook automatically. For a manual global npm install, first locate the package root:

npm root -g

Then add the hook to ~/.claude/settings.json with the matching absolute path:

{
  "hooks": {
    "SubagentStop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node /opt/homebrew/lib/node_modules/@uluops/agent-metrics/dist/hook.js",
            "timeout": 30
          }
        ]
      }
    ],
    "SubagentStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node /opt/homebrew/lib/node_modules/@uluops/agent-metrics/dist/hook.js",
            "timeout": 30
          }
        ]
      }
    ]
  }
}

SubagentStart (v0.12.0) runs the same script, which dispatches on hook_event_name; it records the definition each subagent was spawned with (see Definition capture). The 30 s timeout matches what @uluops/setup installs: the stop path waits up to 3 s for the transcript to settle (tracker 74629040), and the old 10 s left little headroom on a loaded machine.

Definition capture (v0.12.0)

The hook records which agent definition each subagent ran, so -f tracker output carries definition_version without the orchestrator typing it.

  • At SubagentStart the hook resolves the subagent's agent_type to the installed definition whose frontmatter name matches, in Claude Code's documented order: each project .claude/agents from the working directory upward (closest first), then ~/.claude/agents. It records the version, content sha256 and path in ~/.claude/agent-metrics-spawns.jsonl, keyed by agent_id. Claude Code loads the full definition at spawn, so this names what will run.
  • At SubagentStop it re-hashes the file and attaches the capture to the buffer entry.

| Situation | Result | |---|---| | unchanged since spawn | definition recorded, captured_at: "spawn" | | file changed between spawn and stop (a reinstall mid-run) | omitted, definition_unresolved: "changed-during-run" | | file modified within 30s before spawn (Claude Code's reload lag) | omitted, "reload-window" | | no spawn record (SubagentStart hook not configured), file untouched for ≥30s before the agent started | captured at stop, captured_at: "stop" | | no spawn record, file modified later than that (or the start time is unknown) | omitted, "unverified-at-stop" | | a failure inside the capture itself | omitted, "capture-error"; the token metrics are still recorded | | no file / no version: / several same-level files at different versions | omitted, "no-file" / "no-version" / "ambiguous" | | an [agent:name] tag naming a different definition than the one that ran | omitted, "tag-mismatch" | | a plugin-scoped agent type (plugin:name) | omitted, "plugin-scoped" |

Never guess. A missing version costs one run's attribution, which is counted; a wrong one credits another version silently. agent-metrics status reports how many entries carry a version, which causes left the rest without one, how many entries have no agent name, and how many are still spilled.

Limit. The version label is what the file says. A definition edited and reinstalled without a version bump still reports the old label; the sha256 in the buffer is the evidence for a later content comparison.

To enable it, add SubagentStart beside SubagentStop, with the same command (the hook dispatches on hook_event_name):

{
  "hooks": {
    "SubagentStart": [{ "hooks": [{ "type": "command", "command": "node <npm root -g>/@uluops/agent-metrics/dist/hook.js", "timeout": 30 }] }],
    "SubagentStop":  [{ "hooks": [{ "type": "command", "command": "node <npm root -g>/@uluops/agent-metrics/dist/hook.js", "timeout": 30 }] }]
  }
}

Agent Detection

The hook detects the agent name from an explicit tag in the first user message of the transcript:

[agent:code-validator] Validate code quality on this directory

Tag matching is case-insensitive; the resulting name is lowercased.

Name resolution precedence (v0.7.0): explicit [agent:name] tag → the harness-reported agent_type field on the hook payload (when the Claude Code version delivers it — the hook debug-logs payload key names, so presence is observable per capture) → nameless. Nameless entries display with the project directory name, then the agent id. Workflow commands emit tags automatically; direct user invocations can include a tag manually. The agent_type fallback is sanitized before persistence (control characters stripped, capped at 64 chars, v0.7.1): a stray newline would otherwise split a JSONL buffer line and silently drop the entry on read.

Names supplied later at extract time (extract --agent-name/--agent-names) are written back onto the matching buffer entries (best-effort), so earlier nameless captures become name-complete for subsequent queries.

See docs/decisions/0001-explicit-tag-detection.md for the rationale (hardcoded name enumeration was removed in v0.4.0).

Run-Scoped Attribution (v0.8.0)

To attribute token metrics to a specific pipeline run — not every agent in a rolling time window — an orchestrator mints a run token and emits it as a second tag [run:<token>] in the first user message, alongside [agent:name]:

[agent:code-validator] [run:my-project-ship-31948701-01] Validate this directory

The hook persists the token as run_id on the buffer entry, and buffer list --run <token> returns exactly the agents tagged with it. The vocabulary spans three layers, all referring to the same token:

| Layer | Form | Where | |-------|------|-------| | Prompt tag | [run:<token>] | first user message (what the orchestrator emits) | | Buffer field | run_id | BufferEntry / -f json output | | Query | --run <token> (CLI) / queryBuffer({ runId }) (API) | selection |

Token grammar: [a-z0-9][a-z0-9-]{2,63} (lowercase, 3–64 chars, leading digit permitted). A convenient scheme is <project>-<workflow>-<session8>-<nonce>, where <session8> is the first 8 hex chars of the Claude Code session id — e.g. my-project-ship-31948701-01.

run_id is a query key only — it is deliberately not emitted in -f tracker output (the tracker save_run agents[] schema is strict), but it is visible in -f json. See docs/decisions/0004-run-scoped-attribution.md for the full rationale.

Reconciling the count (v0.10.0): a dropped [run:] tag silently drops that agent from a --run result — see ADR-0004's Consequences. The count-check that catches this is now in-artifact, not just a consumer-side obligation:

# Compare attributed agents to an independently-derived expected count
agent-metrics reconcile --run my-project-ship-31948701-01 --expect 9 -f json

Exits 0 on an exact or over-collected match, 1 on a shortfall, 2 on a usage error (see the Core Commands table above for the full exit-code contract). The expected count's provenance is still the caller's responsibility — reconcile mechanizes the comparison, not the expectation; see docs/decisions/0004-run-scoped-attribution.md.

Buffer Commands

# Check buffer status
agent-metrics buffer status

# List all buffered entries
agent-metrics buffer list

# Filter by agent name
agent-metrics buffer list --agent-name code-validator

# Filter to exactly one pipeline run by its run token (v0.8.0)
agent-metrics buffer list --run my-project-ship-31948701-01 -f tracker

# Get all entries for a session (useful for workflows)
agent-metrics buffer session <session-id>

# Output in tracker format for save_run
agent-metrics buffer session <session-id> --format tracker

# Clean up expired entries
agent-metrics buffer clear --expired

# Clear entries for a specific session
agent-metrics buffer clear --session <session-id>

# Clear specific agent IDs
agent-metrics buffer clear --agents a7c642b a03c37d

Workflow Integration

After running a multi-agent workflow:

# Get session ID from parent conversation
SESSION_ID="ea588859-88cd-4511-851a-4fe928cd77c7"

# Get all agents' metrics in tracker format
agent-metrics buffer session $SESSION_ID --format tracker

For run-scoped collection (only the agents of one pipeline run, even when other agents ran concurrently in the same session), tag every agent prompt with [run:<token>] and collect by that token instead of a time window:

# Exactly this run's agents — no cross-session pollution
agent-metrics buffer list --run my-project-ship-31948701-01 -f tracker -p "$TARGET"

This replaces the older --since <duration>m window, which could pull in agents from unrelated concurrent runs.

Output ready for save_run:

[
  {
    "name": "code-validator",
    "agent_id": "a80e24f1c2d3e4f5a",
    "model": "claude-sonnet-4-5-20250929",
    "tokens": { "input_tokens": 63903, "output_tokens": 1510, "cache_creation_tokens": 63236, "cache_read_tokens": 315202, "total_effective_tokens": 65413 },
    "duration_ms": 113126
  },
  {
    "name": "test-architect",
    "agent_id": "ab1c2d3e4f5a6b7c8",
    "model": "claude-sonnet-4-5-20250929",
    "tokens": { "input_tokens": 45200, "output_tokens": 980, "cache_creation_tokens": 44220, "cache_read_tokens": 210000, "total_effective_tokens": 46400 },
    "duration_ms": 87500
  }
]

Manual Integration with Validation Workflows

After running a validation agent:

# Get the agent ID from the Task tool response
AGENT_ID="a80e24f"

# Extract metrics in tracker format
METRICS=$(agent-metrics extract $AGENT_ID --format tracker --agent-name code-validator)

# Use in save_run call
echo $METRICS

Persistence

Global npm installs persist through the npm global prefix. If agent-metrics is no longer on PATH after reinstalling Node.js or changing package managers, reinstall the package:

npm install -g @uluops/agent-metrics

If you use @uluops/setup, rerun npx @uluops/setup to recreate its managed Claude hook files.

File permissions. The buffer, lock, GC-throttle marker, and log files under ~/.claude/ are created with 0600 permissions (owner read/write only) as of this release; freshly created parent directories with 0700. This only applies to files created going forward — an existing buffer hardens itself automatically at its next rewrite (e.g. the next GC), and an existing log at its next rotation. To harden an existing installation by hand: chmod 600 ~/.claude/agent-metrics-*.

Future Enhancements

  • [x] Global installation via npm link
  • [x] Auto-capture via SubagentStop hook
  • [x] Global buffer with TTL-based expiry
  • [x] Agent name auto-detection in report
  • [x] Cache hit rate visibility
  • [x] Batch extract with tracker-ready output
  • [x] Workflow grouping by prompt_id
  • [x] npm publish for npm install -g @uluops/agent-metrics
  • [ ] Redis backend for distributed buffer (Planned — no timeline)
  • [ ] MCP server integration (Planned — no timeline)
  • [ ] Historical trend analysis (Planned — no timeline)
  • [ ] Cost estimation based on token pricing (Planned — no timeline)