agemetry
A ticker plant for coding agents. Local-first, append-only telemetry store for Claude Code, Codex, GitHub Copilot (CLI and VS Code), Cursor and (soon) every other coding agent — with one schema and one SQL view over all of them.
feed handlers tickerplant log HDB query
────────────── ──────────────── ─────────────── ──────────────
Claude Code hooks ─┐
Codex rollouts ─┤
Copilot sessions ─┼─▶ ~/.agemetry/log/ ─▶ ~/.agemetry/hdb/ ─▶ DuckDB `events`
VS Code chat ─┤
Cursor ─┘ YYYY-MM-DD.jsonl date=YYYY-MM-DD/ canned queries,
(today, stdlib-only) events.parquet (zstd) MCP, replay
Your agent logs rot in ~/.codex/sessions, ~/.claude/projects, ~/.copilot,
VS Code's and Cursor's storage. Nothing
compacts, indexes, or joins them. agemetry treats agent activity as what it
is — a high-rate, timestamped, append-only event stream — and stores it the way
market data is stored: a hot log for today, columnar partitions for history,
and a single query surface over both.
Nothing leaves your machine. Credentials that show up in tool output (API keys,
tokens, private keys, passwords in URLs and config) are replaced with
[REDACTED:<kind>] before anything is stored. That covers recognisable
formats, not every possible secret, so --redact stays the rule before sharing.
Data stored before scrubbing existed can be cleaned with agemetry scrub --rewrite. It rewrites the stored files with each credential replaced and
nothing else changed: same rows, ids and timestamps. It's the one exception to
append-only. The agents' own transcripts aren't agemetry's to edit, so rotate
any credential that leaked.
Install
One line, with uv (or pipx install with the same argument):
uv tool install "agemetry[mcp]"
agemetry install # Claude Code hooks for every session
agemetry doctor # checks hooks, adapters, MCP, billing and the store
uv tool upgrade agemetry updates it. From a clone: pip install -e ".[dev,mcp]".
Quick start
agemetry install # hooks for every session, in ~/.claude/settings.json (idempotent)
agemetry doctor # is everything wired up and is data arriving?
agemetry scrub # credentials stored before scrubbing existed (--rewrite removes them)
agemetry ingest # one-off backfill of Claude Code transcripts (and Codex, which every command also reads)
agemetry q cost # cost & tokens by day / provider / model
agemetry q tools # failure rate and p50/p95 latency per tool
agemetry q retries # same tool on same file 3+ times = thrash hotspot
agemetry q sessions # roll-up per session
agemetry q context --session <id> # context-pressure timeline
agemetry sql "SELECT ..." # anything, over the `events` view
agemetry report # one HTML page over everything, opened in your browser
agemetry top # live view of what your agents are doing right now (q quits)
agemetry compact # closed days' JSONL -> Parquet (also done before every command)
No cron or timer is needed: every command, and every MCP tool call, first drains the Claude Code spool, reads new Codex rollout lines and compacts closed days.
The schema is the product
One wide, flat, nullable table. Provider-specific junk goes in raw (JSON) until
two providers need a column. See agemetry/schema.py.
| column | notes |
|---|---|
ts, seq |
µs UTC; seq breaks ties |
event_id |
derived from the source (spool line, rollout line, API message id); re-reading a source after a crash gives the same id, and the events view and compaction keep one row per id |
provider, session_id, parent_session_id |
subagents link to parents |
event_type |
session_start/end · prompt · assistant · tool_call · tool_result · token_usage · diff · permission · error · subagent_start/end · notification |
model, tool, tool_call_id |
tool_call_id joins call ↔ result ↔ duration |
repo, branch, cwd, file, host, user |
the "symbols" you group by |
tokens_in/out/cache_read/cache_write, cost_usd |
from transcripts / token_count events |
duration_ms, lines_added/removed, ok |
tool outcome |
text, raw |
truncated human text; full payload for replay |
Adapters
-
Claude Code — hooks push events live, and
agemetry ingest claudebackfills token usage from existing transcripts. Details below. -
Codex — polls
~/.codex/sessions/**/rollout-*.jsonlfrom saved offsets (checked against Codex CLI 0.166.0).exectool calls carry the shell command they ran; each command is also ashelltool_result with its exit code. Model comes fromturn_context, branch from.git. Injected context (<environment_context>, developer instructions) is anotification, not a prompt. Token counts are normalised to Claude Code's meaning:tokens_inexcludes cached tokens. Codex logs no cost;cost_usdis priced fromprices.json. Unknown lines are kept asnotificationwithrawintact. -
GitHub Copilot (
provider = 'copilot'), two sources;session_start.textsays which client:- Copilot CLI and VS Code's agent host (its
client_name, e.g.vscode-agent-host) — tails~/.copilot/session-state/<id>/events.jsonl; repo and branch from itsworkspace.yaml. Tool calls with success and duration, permission prompts,apply_patchline counts. Copilot reports usage in AI units; one is a $0.01 AI Credit, socost_usdis Copilot's own figure, not a price-table guess. Token totals arrive at session end. - VS Code chat sessions (
vscode-chat) — the files VS Code wrote before its agent host, underworkspaceStorage/*/chatSessions/. Each turn is read once it finishes. Model is the one Copilot actually routed to. Terminal commands carry exit codes and durations; other tools have no success flag in these files. Tokens have no cache split,cost_usdcomes fromcopilotCreditswhere recorded, and branch is unknown.
- Copilot CLI and VS Code's agent host (its
-
Cursor — reads agent and chat sessions from Cursor's own database (
globalStorage/state.vscdb), from a copy so the running app is never touched, and only when it has changed. Repo from the workspace, branch from Cursor's tracked git repos, tool calls with their targets and outcomes (completed, error, rejected, cancelled), lines added and removed per session. Cursor keeps no token counts or costs locally (it bills on its servers), no tool end times and no exit codes, so those stay empty.
Every adapter keeps the original payload in raw, so a renamed upstream field
costs a null column, not lost data.
Claude Code hooks
A hook runs on every tool call, so it has to be close to free. The hook does no work beyond recording:
- Per tool call (
PostToolUse,UserPromptSubmit, ...): a ~60-line C program,agemetry/hook/spool.c, appends the raw payload with a timestamp to~/.agemetry/spool/YYYY-MM-DD.jsonlin a singlewrite()and exits 0. It never parses its input. About 2 ms, against 1.3 ms for spawning/usr/bin/trueand ~32 ms for a Python hook. - Once per turn (
Stop,SubagentStop,SessionEnd): the Python hook drains the spool into events and tails the transcript for token usage. - Before any query, the CLI drains whatever is left.
agemetry install compiles the C program with the system cc into
~/.agemetry/bin/. Without a compiler (or with --python-only) every hook
runs the Python version instead: same results, ~32 ms per call.
| option | choices |
|---|---|
--scope |
user (default, every session on the machine), local (this repo, .claude/settings.local.json, not committed), project (this repo, .claude/settings.json, committed) |
--events |
minimal (default: one hook per tool call) or full (adds PreToolUse, PermissionRequest, Notification, PreCompact) |
agemetry uninstall --scope <scope> removes only agemetry's hooks. Hooks you
wrote yourself are left alone. Windows currently gets the Python hook.
Billing: what you actually pay
cost_usd on every event is the usage at list prices: what those tokens
would cost on the API. Many people don't pay that. Subscriptions cost the same
however much you use them, and enterprises negotiate rates. Tell agemetry your
plan and it prices everything accordingly when you read. Stored events never
change, so you can edit a plan at any time.
agemetry billing set claude_code --subscription 200 --name Max --start 2026-09-01
agemetry billing set claude_code --subscription 90 --currency GBP --fx 1.34 # priced in pounds
agemetry billing set copilot --subscription 19 --name Business
agemetry billing set codex --discount 20 # or --rate 0.8: 80% of list price
agemetry billing # last 30 days: list value vs paid, per provider
agemetry billing unset codex
A subscription is prorated over whatever range you look at: 30 days of $200
a month is $197.13. Plans live in ~/.agemetry/billing.json.
To see every amount in your own currency, in the CLI and the report, set a
display currency. Stored data stays in dollars, and USD turns it back off:
agemetry billing currency GBP --fx 1.34
List prices are in US dollars, so a plan priced in another currency is
converted at the rate you give with --fx (US dollars per unit). The plan
keeps the original amount, the rate and the date it was set. agemetry
doesn't fetch rates; set the plan again to update one.
Usage limits (recorded, not shown yet)
agemetry records subscription usage windows as usage_limit events:
- Claude's 5-hour and 7-day windows, from the figures Claude Code caches in
~/.claude.json - Codex's limits, from its rollouts
agemetry q limitslists the latest readings.
They are kept out of the report, agemetry top, the MCP tools and alerts.
Claude Code refreshes its cache only now and then, and a reading can be days
old (one on this machine was 8 days old, and both windows had reset since). A
stale "29% used" shown as current, or an alert on it, would mislead. The
history is still recorded, so this can come back once there's a live source.
Outcomes and cost per commit
agemetry q outcomes # sessions by outcome: PR opened, committed, edited not committed, explored
agemetry q commits # recent commits with the agent cost behind each
agemetry reads git log (read-only) for every repo its agents worked in, found
from the sessions' working directories. A linked worktree counts as its main
repo. Commits go in their own commits view, not in events.
- A commit's cost is the agent work in its repo since the previous commit.
- A session's outcome is the strongest of these:
- PR opened: a successful
gh pr createin a shell tool - committed: its repo got a commit while it ran or within 30 minutes after
- edited, not committed
- explored
- PR opened: a successful
- Subagent work counts towards its parent session.
The report's Outcomes card shows sessions by outcome, and cost per commit at list price and under your plans. Commit messages are stored, scrubbed, for the CLI, and never put in the report.
Live view
agemetry top # refreshes every 3 s; q quits, r refreshes now
agemetry top --once # one snapshot, for scripts
- Header: today's spend at list prices, in your display currency, and today's active time.
- One row per session active in the last 30 minutes: repo and branch, time since its last event, model calls, tool calls and failures, cost, current context size and last tool. Subagents sit under their parent.
Alerts
agemetry alerts set --session-cost 20 # a session passes $20 at list prices
agemetry alerts set --daily-cost 50 # a day passes $50 at list prices
agemetry alerts set --failures 15 # a session has 15 failed tool calls
agemetry alerts # rules and recent alerts (0 turns a rule off)
agemetry alerts test # see what a notification looks like
Rules are checked at the end of every Claude Code turn, inside the hook that
already runs then. The check is stdlib only and uses no database. Each alert
fires once per session or day, as a local desktop notification
(macOS, or notify-send on Linux). Nothing is sent over the network.
Time in use
agemetry q time # per day and provider: active hours, agent hours, tokens, cost per active hour
- Active time is wall-clock: the stretches where events arrive no more than 5 minutes apart. Parallel sessions and subagents are merged, so it can never exceed the clock.
- Agent time sums each session's active time, so three agents working for an hour count three hours. The gap between the two is how much your agents ran in parallel.
Report
agemetry report # writes ~/.agemetry/report.html and opens it
agemetry report --redact # repo, branch and file names become repo-1, branch-1, file-1
agemetry report --out x.html --no-open
One self-contained HTML file covering:
- what you paid under your plans, next to the list-price value (see Billing below)
- active time and cost per active hour, sessions, tool calls, failure rate and tokens
- API value, tokens or active time over time, by provider
- spend by model, across providers
- spend by repo, each opening to show its branches
- tool reliability (failure rate, p50/p95 latency)
- a session timeline: context, tool calls (failures marked) and prompts on active time
- retry hotspots
A date-range filter (7/30/90 days, all time) re-scopes every number, and
#range=30&m=time in the URL opens on that range and measure. Every chart has a table view.
Light and dark themes are both supported. The page embeds aggregates only, never prompt or
response text, and loads nothing from the network. Use --redact before
sharing a screenshot.
Team mode
agemetry team init ~/Dropbox/agemetry-team # once per machine: the shared folder and your member name
agemetry team push # share your last 90 days of aggregates
agemetry team report # everyone in the folder, in one page
agemetry team status # who has pushed, and when
agemetry team demo --members 3 # synthetic teammates, to try it on your own
agemetry team demo --remove
The team folder is any directory everyone can reach: a synced folder or a
network share. Each member owns members/<member>/, replaced whole on every
push, with three small Parquet files and a meta.json:
- daily sessions, prompts, tool calls and failures, tokens, list-price cost and lines changed, by provider, model, repo and branch
- daily active and agent time, and what you paid under your own plans
- your commits per repo per day, and the agent cost behind them
Only aggregates leave the machine: no prompts, responses, code, tool output,
file paths, commit messages or session ids. Repo and branch names are shared,
because that's what a team report groups by; team push --redact replaces
them with placeholders. Only commits by your git email (git config user.email, or team init --email) are shared, so a repo two people work in
isn't counted twice.
The team report shows paid and list-price spend, active time, sessions,
commits and failure rate per member, spend over time stacked by member, and
spend by repo and by model across the team. team demo builds teammates from
your own aggregates: different amounts, days, model mix and plans, renamed
repos, marked as demo on the page.
Replay: would another model have done the same?
agemetry replay --list # recent Claude Code sessions
agemetry replay <session> --steps 10 # dry run: the plan and an estimated cost, nothing sent
agemetry replay <session> --model claude-sonnet-5-5 --run # calls the Anthropic API
Replay rebuilds a recorded Claude Code session from its transcript and samples its decision points (each API call the agent made). At each one it gives another model the conversation as it stood, with the session's own system prompt and tools, and compares that model's next action with what was recorded:
- same: the same tool on the same file, command or pattern, or both stopped
- same tool: the same tool on a different target
- different: anything else
The recorded tool results stand in for running the tools, so nothing is executed and a replay can be repeated.
How faithful a replay is:
- The system prompt, tool definitions, effort and max_tokens come from the transcript's own request records (Claude Code 2.1.29x and later). Decisions without them are skipped and counted.
- After a compaction, the replayed model sees the summary onward, as the original did.
- Messages are rebuilt from the main conversation. Per-turn context Claude Code injects outside it is not.
- The original model's thinking isn't replayed.
A dry run reads the transcript locally and sends nothing. --run needs
pip install -e ".[replay]" and an Anthropic API key or ant auth login. It
sends that session's content (code, file contents, command output) to Anthropic
and costs about the dry run's estimate. Prompt caching keeps it low, because
each step's request extends the previous one. Results go to
~/.agemetry/replays/.
MCP server
Lets the agent ask about itself: "what has this branch cost me?"
pip install -e ".[mcp]"
claude mcp add agemetry -- agemetry mcp # Claude Code
For Codex, add to ~/.codex/config.toml:
[mcp_servers.agemetry]
command = "agemetry"
args = ["mcp"]
Tools: branch_summary (pass the working directory), list_queries /
run_query (the canned queries above), describe_events, and sql. Every
tool is read-only. sql accepts one SELECT, on a DuckDB connection that can
read only log/ and hdb/ in the store: no other files, no network, no
extensions. Results are capped at 200 rows and 2000 characters per value.
branch_summary reports both the list-price value and what the branch cost
under your billing plans, shown in your display currency. A subscription is
shared out by use: the branch's list value × (paid ÷ list value) for that
provider over the same days.
Roadmap (in order)
- OpenTelemetry receiver: a local OTLP endpoint, so any agent that emits OpenTelemetry is captured without its own adapter.
- Report polish: the screenshot that gets it shared. Storage is invisible; the picture isn't.
- Adapters: Claude Code, Codex, Copilot (CLI, VS Code) and Cursor are checked against real sessions. Next: Gemini CLI, OpenCode.
- Replay: decision replay against another model is in (above). Next: a same-model baseline, replay results in the report, and multi-step replays that let the new model's own choices play out.
- Team mode: a shared team folder and team report are in (above). Next: push to S3/R2 directly.
Development
CI runs the tests on Linux with Python 3.10 and 3.13, and with the oldest supported duckdb (1.2.0), in one job. macOS runs weekly and on demand.
pip install -e ".[dev]"
pytest # end-to-end, plus the C hook under address/undefined-behaviour sanitizers
AGEMETRY_HOME=/tmp/x agemetry stats # point at a scratch store
Licensed under the Apache License 2.0. See LICENSE.
Metadata
Release files for agemetry 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| agemetry-0.0.1.tar.gz | 136.2 kB | Without provenance | Details for agemetry-0.0.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| agemetry-0.0.1-py3-none-any.whl | Python 3 | none | any | Details for agemetry-0.0.1-py3-none-any.whl |
Total release size: 249.0 kB
Release files / agemetry-0.0.1.tar.gz
| Download URL | agemetry-0.0.1.tar.gz |
|---|---|
| Size | 136.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bdb7e31cfcc8e27ad4349cec78bcd0e679ea11d6a6c8ba5235449087b4a124cb
|
|
BLAKE2b-256 checksum How to use checksums |
f7e2042fc242103aafff47ccd54e1f94ea86ed9369696dd9a34887b6a09b8adb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.2
|
Release files / agemetry-0.0.1-py3-none-any.whl
| Download URL | agemetry-0.0.1-py3-none-any.whl |
|---|---|
| Size | 112.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f2a73016d53782efb67dcf5e928bfee79658157812930d441cd8b78c9d82eb81
|
|
BLAKE2b-256 checksum How to use checksums |
0ceff1f453fb08eb70a65de3adb9ebcb1aaaaf88ce87fffcfdfa9cb35bf471b7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.12.2
|
Security for agemetry 0.0.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.