Not affiliated. AI Activity is an independent, unofficial project. It is not affiliated with, endorsed or sponsored by Anthropic, OpenAI, Anysphere, Google or the OpenCode project. Claude, Claude Code, Codex, Cursor, Antigravity, OpenCode and the other product names used here are trademarks of their respective owners, named only to identify the tools whose usage the dashboard measures. It reads local usage files or receives metrics through the tools' hooks; it does not call their APIs on your behalf or work around their limits. Using those tools stays subject to their own terms.
The server counts visits to its own pages, for the admin panel only, and
sends nothing to a third party. No cookie is set for it. Each browser
keeps a random id in localStorage for 13 months, so a return visit is
told apart from a new one; the server stores only keyed hashes of it, one
per day that cannot be linked to the next, plus the first and last day
seen. What is counted: the page category (never the address or the
profile name), the referring site's host name, sign-ups, and reads of the
public API by other sites and programs. IP addresses and user-agent
strings are never stored: without the id, a hash of them under a key that
changes every day and is never written to disk stands for the visitor.
Daily counts are deleted after 90 days.
Sign in and open Friends from the avatar menu to see the people you follow on GitHub who also have an enabled AI Activity profile. Each entry links to that public profile and shows measured tokens, conversations and last activity for the past seven days. The server reads GitHub's public following list using the OAuth app's client ID and secret, without requesting an OAuth scope or keeping a user's GitHub token. If GitHub is unavailable, the page offers a retry.
Next to the token totals, profiles and the leaderboard show an estimated API-equivalent value: what the measured tokens would cost at the providers' published retail API rates, in USD. It is a valuation of usage, whatever paid for it (a subscription or an API key): not what you paid, not a saving, not what serving it costs the provider. Nothing new is asked of you; no plan, invoice or key is needed.
- Where rates come from, first match wins:
shared/pricing.json, the priority file: verified official rates, each with its source and the day it took effect, and aliases that price a model as another one (OpenCode's freeopencode/muse-spark-1.3-contributor-freeat Meta's paidmeta/muse-spark-1.3-contributorrate). Edit this file to add a rate or an alias; the server refuses to start on an invalid one (npm testchecks it too).- LiteLLM's price list
for every other model: the server downloads it at most once a day and
keeps it next to the database (
litellm-prices.json), so a restart or GitHub being down changes nothing. Only the provider's own rate is used, never a reseller's for the same model name. These are community rates: values using them say so. They have no effective dates: the copy in use prices all of a model's history, so a LiteLLM price change moves that model's past values too (verified rates in the priority file do not).LITELLM_PRICES_URLpoints elsewhere, or empty turns it off. - Nothing: models without a known rate (Codex's
codex-auto-review, a ChatGPT-only model; a local model) are counted as unpriced, never valued at $0: a figure that leaves some out shows the share of tokens priced ("93 % priced"), and one with nothing priced shows "—". Admin panel → Pricing lists them, with their tokens and why, and the LiteLLM copy in use.
- What it is computed from: each message's model, input, output, cache read and cache write tokens (cached input and reasoning are never counted twice), Anthropic's 5-minute and 1-hour cache writes (2× input instead of 1.25×), Claude Code's fast mode, Anthropic's US-only inference (1.1×), Codex's service tier (Fast/priority, Flex) and the long-context rates where a model has them (Sonnet 4 and 4.5 above 200K prompt tokens; GPT-5.4, GPT-5.5, GPT-5.6 and GPT-6 above 272K).
- Versions: the priority file has a
version(shown with the values), changed with every edit. Usage is priced at the rate of its day; usage older than a model's oldest published rate is priced at that rate and flagged. The dashboard and the leaderboard use the same rates, so their figures for a period agree. - Assumptions it flags: messages recorded before the collectors sent the cache durations (Claude Code collector 4) are priced at the cheaper 5-minute rate, shown as "≥" (a lower bound). Updated collectors send their history once more, which fills that in. Batch discounts, Codex data residency surcharges, server tool fees (web search) and negotiated discounts are not measured, so not included.
- Codex Fast mode is valued as requested. Under load, OpenAI can serve a Fast (priority) request at Standard speed and bill it at Standard rates, but the rollout only records the tier Codex asked for. Such responses are valued at Fast rates: the value can be slightly too high. Nothing in the local files tells them apart today.
- Leaderboard: rank by tokens (the default) or by API value over the same period; accounts with nothing priced rank last.
On your own profile, use Edit layout at the top right of Dashboard panels.
While you edit, every card shows sample data (the fictional /demo data), so
you see how the dashboard will look. Arrange it by dragging the cards: drop a
card next to another to share its row (up to three cards per row), above or
below a row to start a new one, or onto a card of a full row to swap them.
An empty box shows where the card will land, and the other cards move aside.
Drag the bar between two cards to set their widths (half, ⅔ · ⅓ or ⅓ · ⅔).
The drawer at the bottom holds the panels not shown (every tool and four
usage cards: Today by tool, Today by hour, Best day and Leaderboard · 7
days): drag one onto the dashboard to add it, or drag a card onto the drawer
to remove it. Claude Code, Codex and Antigravity each have a quotas and a
details panel, which can be shown together. On a touch screen, press and
hold a card to pick it up. With a keyboard, focus a card, press Space, move
it with the arrow keys and press Space again (Escape cancels). Save
layout publishes it on your public profile; visitors see it but cannot
change it. Until you save one, your profile follows the default layout, new
tools included. The public /demo page shows every panel with fictional,
labeled data.
Create a device key (Settings → Devices),
then on that machine, as the user who runs the tools (no sudo, no
administrator shell):
# Linux, macOS
curl -fsSL <server>/install.sh | AI_ACTIVITY_URL=<server> AI_ACTIVITY_KEY=<device key> sh# Windows (PowerShell)
$env:AI_ACTIVITY_URL="<server>"; $env:AI_ACTIVITY_KEY="<device key>"; irm <server>/install.ps1 | iexAfter you create a key, Settings → Devices offers buttons to copy the Linux/macOS command, Windows command, or an AI setup prompt. Each existing device has a Set up action for the same options and a way to copy its key again. The prompt gives an AI assistant both commands, asks it to use the one that matches the device, and tells it to explain the installer's output and any follow-up steps. The installer detects tools automatically; the prompt does not ask you to select them. You can review the full prompt before copying. It includes the device ingestion key: pasting it into an AI service shares that key with the service. Revoke the key in Settings → Devices if it is exposed.
The copy buttons include the key in each command (your profile links to
Settings → Devices under the tools). The key is passed in the environment,
never in a URL, and ends up only in the installed collectors (files readable by you alone on
Linux/macOS). Both scripts need Python 3 (python3; on Windows python
or the py launcher) and install the collectors of the tools they find
(claude / codex / cursor / agy / opencode on the PATH, or their config
folders), as the sections below describe:
- Claude Code:
~/.claude/ai-activity-claude-code.py, its five hooks (tokens) and thestatusLine(quotas) in~/.claude/settings.json, next to your other hooks. Another status line already set is left alone: the tokens are still sent, the quotas are not (AI_ACTIVITY_FORCE=1replaces it: Claude Code runs only one). Restart Claude Code. - Codex:
~/.codex/ai-activity-codex.pyand its three hooks in~/.codex/hooks.json, next to your other hooks. Review them once with/hooks. - Cursor:
~/.cursor/hooks/ai-activity-cursor.pyandafterAgentResponsein~/.cursor/hooks.json, next to your other hooks. Check Settings → Hooks. Only new Agent turns reporting tokens are collected. - Antigravity:
~/.gemini/ai-activity-antigravity.pyand the namedai-activityhook in~/.gemini/config/hooks.json. Restart it and check the hook is enabled. Quotas stay off (AI_ACTIVITY_ANTIGRAVITY_QUOTAS). - OpenCode: the collector and plugin in
~/.config/opencode. Restart it.
On Linux/macOS the commands are the ones below (python3 ~/…; on macOS,
which has no setsid, Codex runs the script with --hook). On Windows they
name the absolute path of the Python that ran the installer, so the tools
find it whatever their PATH (OpenCode's plugin still runs python).
Other entries of those files are kept, and a file that is a symlink (a
dotfiles repo) stays one: its target is updated. If one of them is not
valid JSON, nothing is installed until it is fixed.
AI_ACTIVITY_TOOLS=claude-code,codex
($env:AI_ACTIVITY_TOOLS=… on Windows) picks the tools instead. Running it
again only updates what changed (a new key or server URL, a newer
collector), never duplicates a hook. It checks that the server takes the
key first, refuses a <server> that redirects (use the address it
redirects to, e.g. https://: the collectors do not follow redirects), and
refuses to run as root unless AI_ACTIVITY_ALLOW_ROOT=1.
- Create a device key in the dashboard, Settings → Devices (one key per machine, it serves every tool on it; Copy key there gives it back any time).
- Copy
collectors/claude-code.pyto~/.claude/ai-activity-claude-code.pyon the device and replace<server>(e.g.http://localhost:3000or your tunnel URL) and<device key>at its top (or setAI_ACTIVITY_URL/AI_ACTIVITY_KEYin the environment Claude Code runs in). - Add this to
~/.claude/settings.json(merge the hooks into any you already have), then restart Claude Code:
"hooks": {
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
"PostToolUse": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
"Stop": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
"StopFailure": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}],
"SessionEnd": [{"hooks": [{"type": "command", "command": "python3 ~/.claude/ai-activity-claude-code.py --hook", "timeout": 10}]}]
},
"statusLine": {
"type": "command",
"command": "python3 ~/.claude/ai-activity-claude-code.py"
}On Windows, use this instead, replacing <user> with your Windows user
directory name (backslashes and quotes are already JSON-escaped):
"hooks": {
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
"PostToolUse": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
"Stop": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
"StopFailure": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}],
"SessionEnd": [{"hooks": [{"type": "command", "command": "python", "args": ["C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py", "--hook"], "timeout": 10}]}]
},
"statusLine": {
"type": "command",
"command": "python \"C:\\Users\\<user>\\.claude\\ai-activity-claude-code.py\""
}It needs Python 3 (standard library only) and runs as your user: no root,
nothing printed in the status line. Use the absolute path of the
interpreter if it is not on Claude Code's PATH (python3 -c 'import sys; print(sys.executable)', or python -c "import sys; print(sys.executable)"
on Windows, JSON-escaped like the script path).
Windows hooks use command plus args: Claude launches Python directly,
so they work when Claude is started from PowerShell or cmd, with or without
Git Bash. An absolute command path must have no extra shell quotes. The
Windows installer also creates ai-activity-claude-code.ps1 for the status
line, calling the installing Python interpreter with PowerShell's & operator.
It invokes that wrapper explicitly with powershell.exe -NoProfile -File,
so spaced interpreter paths work from either shell too.
The hooks send the tokens, the status line the quotas. Claude Code
only gives its 5-hour and 7-day quotas and the context fill to the status
line; the tokens are in the transcripts, which any hook can read. So the
hooks alone count every token, headless runs (claude -p, the Agent SDK)
included, and keep working next to a status line of your own: only the
quotas are then missing ("Unavailable").
Replacing an earlier install (the statusLine alone, or the former
setsid -f python3 -c "…" one-liner): run the install command again, or
add the hooks and swap the statusLine command for the one above. The
one-liner's first run sends every transcript again (its offsets did not
say which server they were for); the server stores each message id once,
so nothing is counted twice or missed.
What the hooks do (a prompt, every tool call, the end of a turn, a turn ended by an API error such as a rate limit, the end of a session). A turn you interrupt fires none of them: its tokens go with the next prompt, or at exit:
- Read what was added to every transcript under
~/.claude/projects(sessions and subagents, all projects) since the last successful upload, and send one entry per Anthropic message id with its token counts. Prompts and replies never leave the device, only ids, model, time (with the device's UTC offset at that time) and counts. The hook's own input is never sent. - The first run therefore sends every transcript still on disk (Claude Code keeps about 30 days by default): that is the import of past sessions. It also replaces the rows the old snapshot collector sent for those sessions, which counted most API calls twice.
- How far each file was sent is kept in
~/.cache/ai-activity/offsets.json, per server and device key, and only moves forward once the server accepted everything, so nothing is lost while the server is down: the next run sends the backlog. A new server or key starts from nothing, so its first run sends the whole history. Delete that file to send everything again (safe: the server stores each message id once). - The server stores each message id once. Claude Code sometimes writes a partial entry (a few output tokens) before the final one; the final counts replace it.
- The script answers at once and starts the upload detached (its own
session on Linux/macOS; on Windows out of the console, the process group
and, when Windows permits it, the parent job), so it never slows Claude
Code down and survives it being cancelled. Uploads wait for each other
(a lock in
~/.cache/ai-activity); since tool calls come fast, at most one more waits behind the active one and the others exit at once. A run gives up after 15 minutes.
What the status line does on every refresh: sends the 5-hour and 7-day
quotas and the context fill, detached the same way, unless the same values
went to the same server in the last 5 minutes (status.json in
~/.cache/ai-activity): it refreshes several times a second while Claude
Code works, and shares the device's request budget with the hooks. It
never reads the transcripts, and prints nothing.
While a POST is in flight, newer observations are saved per session and
coalesced; the active uploader sends the latest without needing another
refresh. Failed observations stay queued for the next refresh or hook.
The last context for each of the 100 most recent sessions is retained and
reapplied after its tokens arrive, even if the status was uploaded first.
Days are local, like GitHub's contribution calendar. Each entry carries
the device's UTC offset when it happened (daylight saving included), and
counts on that local day for every visitor: it never moves afterwards,
even if you travel. "Today" and the streak end on the day it is at the
offset of your latest entry. Entries sent before collectors had offsets
count as UTC days; delete ~/.cache/ai-activity/offsets.json (and
codex.json, opencode.json) once to resend the history with offsets: nothing is counted
twice, the server only adds the missing offsets.
The tool is part of the URL (/api/ingest/claude-code, /api/ingest/codex,
/api/ingest/opencode, /api/ingest/antigravity);
a bare /api/ingest answers 404. See AGENTS.md §5 for the payload contract.
- Create a device key as above (the same key can serve both tools).
- Copy
collectors/codex.pyto~/.codex/ai-activity-codex.pyon the device and replace<server>and<device key>at its top (or setAI_ACTIVITY_URL/AI_ACTIVITY_KEYin the environment Codex runs in). - Add the hooks to
~/.codex/hooks.json(the same command four times):
~/.codex/hooks.json:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 10 }] }
],
"PostToolUse": [
{ "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 10 }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 10 }] }
],
"SessionEnd": [
{ "hooks": [{ "type": "command", "command": "setsid -f python3 ~/.codex/ai-activity-codex.py >/dev/null 2>&1 </dev/null; echo '{}'", "timeout": 3 }] }
]
}
}On macOS, replace each command value above with
python3 ~/.codex/ai-activity-codex.py --hook. The script answers the hook
and starts its upload in a detached process without setsid.
On Windows, use this instead, replacing <user> with your Windows user
directory name (--hook answers Codex and starts the upload detached).
These commands target PowerShell: & is its call operator and is required
before a quoted executable path. If Python is not on Codex's PATH, replace
python with its quoted absolute path, JSON-escaped as shown below:
%USERPROFILE%\.codex\hooks.json:
{
"hooks": {
"UserPromptSubmit": [
{ "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 10 }] }
],
"PostToolUse": [
{ "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 10 }] }
],
"Stop": [
{ "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 10 }] }
],
"SessionEnd": [
{ "hooks": [{ "type": "command", "command": "& python \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook", "timeout": 3 }] }
]
}
}For example, a Python installation with spaces in its path uses this JSON command for each of the four hooks:
{ "command": "& \"C:\\Program Files\\Python312\\python.exe\" \"C:\\Users\\<user>\\.codex\\ai-activity-codex.py\" --hook" }Use paths that exist on your device. The equivalent command entered directly in PowerShell is:
& "C:\Program Files\Python312\python.exe" "C:\Users\<user>\.codex\ai-activity-codex.py" --hookIf your hook runner uses cmd.exe, omit &; it is PowerShell syntax.
Claude Code's Windows hooks above use direct executable/argument form,
which needs neither PowerShell's & nor cmd-specific quoting.
Codex asks you to review a new hook once (/hooks) before running it.
Repeated hook exited with code 1 errors can mean PowerShell rejected a
quoted executable path before Python ran. Check the call operator and JSON
escaping above; a successful --hook invocation returns exit code 0 and {}.
Re-run the one-command installer to update all three AI Activity Codex hooks;
it preserves your other hooks.
If hooks succeed but console windows still flash, Codex CLI's shared background server can be responsible. The following workaround stopped the flashes in the Windows setup reported in AI Activity #173:
codex --no-daemon
# To resume a session:
codex --no-daemon resumeThis runs the CLI session without the shared background server. Keep the
collector hooks enabled: automatic uploads, historical imports and archived
sessions continue to work. Check codex --help for --no-daemon in your
installed version. This is a CLI workaround, not a guaranteed fix for every
source of console flashes or for the desktop app. Switching to pythonw.exe
alone did not eliminate the flashes in that setup. Upstream reports:
openai/codex #48422 and
openai/codex #48074.
When you update ~/.codex/hooks.json, re-copy collectors/codex.py to
~/.codex/ai-activity-codex.py at the same time: an older script blocks on
codex.lock, so the frequent PostToolUse runs would pile up behind it.
What it does after every tool call and at the end of every turn:
- Reads what was added to every rollout under
~/.codex/sessionsand~/.codex/archived_sessionssince the last successful upload. Every Codex front end writes those files (CLI,codex exec, the IDE extension and the desktop app), so tasks started anywhere are counted, including ones already in progress when the hook was added. - Sends one entry per model response (
token_usage_record, keyed by itsresp_…id) with its token counts, model and the machine's UTC offset at that time. Rollouts from Codex versions without those records are read from theirtoken_countlines, one per response, keyed by the thread's running total so a repeated line counts once. Prompts, replies and tool output never leave the device. - The first run sends every rollout still on disk: that is the import of past sessions.
- Also sends the 5-hour and weekly rate limits and the context fill Codex recorded with each response, dated when Codex measured them. A limit snapshot written without token counts (e.g. when a turn failed) is sent too, so an exhausted quota still records its final value.
- Codex has no
StopFailurehook. ItsStophook does not fire when Codex stops on a rate limit (upstream Codex bug), which would leave that final snapshot unsent: theUserPromptSubmithook runs the same script on your next prompt and picks it up. If Codex is driven without prompts (codex exec), run the script by hand or from cron after hitting a limit instead. - How far each file was sent is kept in
~/.cache/ai-activity/codex.json, per server and device key (a new one gets the whole history), and only moves forward once the server accepted everything, so nothing is lost while the server is down (no separate spool needed): the next turn sends the backlog with its original times. Delete that file to send everything again (safe: the server stores each response once). PostToolUsesends a long turn's usage while it runs (Codex writes each response to the rollout as it comes), so the dashboard follows the turn instead of catching up at its end. A reply without any tool call waits forStop.SessionEndsends any remaining rollout lines when the main session ends. Codex may delay this event until the session has been idle for 30 minutes; it is not an immediate replacement forUserPromptSubmitafter a failed turn.- On Linux,
setsid -fdetaches the upload so Codex goes on at once;echo '{}'is the (empty) JSON answer Codex expects from a hook. On macOS and Windows,--hookprints that answer and detaches the worker. One run at a time, with at most one waiting behind it (it reads the rollouts once its turn comes, so any other run can stop at once); a run gives up after 15 minutes. The script is idempotent: it can also run by hand or from cron.
- Create a device key as above (the same key serves every tool).
- Copy
collectors/antigravity.pyto~/.gemini/ai-activity-antigravity.py. Replace<server>and<device key>at its top, or setAI_ACTIVITY_URLandAI_ACTIVITY_KEYin the environment Antigravity runs in. Python 3 with the standard-library SQLite module is required. - Merge this named hook into
~/.gemini/config/hooks.json(keep existing hooks). For Linux/macOS:
{
"ai-activity": {
"enabled": true,
"PostInvocation": [
{
"type": "command",
"command": "python3 ~/.gemini/ai-activity-antigravity.py --post-invocation",
"timeout": 10
}
],
"Stop": [
{
"type": "command",
"command": "python3 ~/.gemini/ai-activity-antigravity.py --hook",
"timeout": 10
}
]
}
}For Windows, use 8.3 short paths with no quotes instead, replacing <user>
with your Windows user directory name. Backslashes below are already
JSON-escaped. Quotes would seem natural here, but agy's hook runner splits
the command naively on spaces and keeps the quotes in the pieces, so a
quoted path never resolves; the one-command install writes these short paths
for you (find one with cmd /c for %I in ("<path>") do @echo %~sI):
{
"ai-activity": {
"enabled": true,
"PostInvocation": [
{
"type": "command",
"command": "C:\\PROGRA~1\\Python312\\python.exe C:\\Users\\<user>\\.gemini\\ai-activity-antigravity.py --post-invocation",
"timeout": 10
}
],
"Stop": [
{
"type": "command",
"command": "C:\\PROGRA~1\\Python312\\python.exe C:\\Users\\<user>\\.gemini\\ai-activity-antigravity.py --hook",
"timeout": 10
}
]
}
}Use absolute paths to both Python and the collector if Python is not
on Antigravity's PATH (desktop apps can inherit a different PATH from your
terminal). Find the interpreter with python3 -c 'import sys; print(sys.executable)'
on Linux/macOS, or python -c "import sys; print(sys.executable)" on Windows.
Quote paths containing spaces; on Windows JSON-escape the interpreter path
in the same way as the collector path. Configure the URL/key in the copied
script if Antigravity does not inherit your terminal's environment; use a
stable server URL for ongoing collection. Keep the device key out of the
hook command and never commit the configured copy.
The Antigravity hook configuration
is shared by Antigravity 2.0, CLI, and IDE. PostInvocation refreshes after
each model invocation during a turn; Stop refreshes when the execution
loop ends. Both return immediately and launch a detached collector, which
waits two seconds for metadata to flush after acquiring the collection lock.
At most one hook worker collects and one waits; additional
hooks coalesce into that waiting pass, which reads a fresh snapshot. A hook
during collection can queue the next pass, so the final Stop update is included.
Only one uploads at a time. The quota probe runs after the collection lock
is released, so a queued worker never waits for it. Updates need no manual
command after setup, while Antigravity is running and these hooks are enabled.
- Restart Antigravity, then confirm ai-activity is enabled:
/hooksin CLI, Settings → Customizations → Hooks in Antigravity 2.0, or … → Customizations → Hooks in the IDE agent side panel. - Run the copied script without either hook flag once to import history and see diagnostics. Exit code 0 means supported entries were processed; warnings can still indicate skipped unsupported rows or databases, or an unavailable quota report (with its reason). Exit code 1 means a busy database or an upload failure; fix it and run again.
- Complete a new Antigravity turn and leave the dashboard open. Its existing 5-second refresh should show supported persisted usage after collection. If it does not, check the hook is loaded, Python and script paths resolve in Antigravity, the configured URL/key are correct, and a manual run works. Unsupported database formats may still produce no usage; see below.
For retries while Antigravity is idle, or a version that persists metadata later than its hooks run, you can additionally schedule the script without hook flags every minute (cron on Linux/macOS or Task Scheduler on Windows). Use the same user, configured script, and absolute interpreter/script paths; on Windows set the task not to start another instance if already running. Hooks and scheduled runs share checkpoints and safely deduplicate uploads. On Windows, workers detach from the console and create a new process group. They also break away from the parent job when Windows permits it; jobs that forbid breakaway fall back to console/group detachment. If the host kills its entire job, use the scheduled retry above to cover that restriction.
What it does:
- Reads existing SQLite databases only under
~/.gemini/{antigravity,antigravity-cli,antigravity-ide}/conversations/.GEMINI_CLI_HOMEcan replace~/.gemini. Support depends on a database containing the recognizedgen_metadatatable; encrypted/legacy conversation files and transcript-only versions are not supported. - Selects generation metadata and, when needed, step metadata from a read-only snapshot. Never selects conversation text, prompts, responses, tool output, workspace paths, or authentication data.
- Sends ids, recorded model (unknown stays unknown), token counts, the
original generation timestamp, and this machine's UTC offset at that
time. Input is uncached input; cached input is separate; text and
thinking output are added once. Antigravity CLI 1.2.12 writes a constant
in
1.4.1that is not an input counter and repeats total output in1.4.3: both are ignored, so a first turn reports 12 input / 7 output, not 1048. Subagent databases count as separate conversations because parent attribution is unavailable. - Imports supported history, then skips every database whose stamp is
unchanged since all of it was accepted. The stamp covers the database and
its WAL: mtime, size, ctime, inode, the database header's change counter
and the WAL header's salts. A changed database is read again in full (edits
to older rows included), but only new responses, or ones with more output
tokens, are sent. Checkpoints in
~/.cache/ai-activity/antigravity.jsonhold, per server and device key and per conversation (hashed path), that stamp and the output tokens accepted per response id; a batch is recorded only once accepted, and deleted conversations are forgotten. A malformed checkpoint file starts over (the server deduplicates the replay); removing it replays history too. Switching server or device key automatically starts a new import. - A busy (locked) database fails the run and is read again next time. Any
other unreadable or unsupported database (not SQLite, another layout, a
WAL database whose
-shmfile cannot be created) is skipped with a warning until its stamp changes. - Unconfigured URL/key placeholders exit before reading history. The first
HTTP/network failure on a usage upload stops the pass, including quota
probing. HTTP 429/503 honor
Retry-After(seconds or HTTP date; bounded to one day, with a one-minute fallback). Device keys are never forwarded through redirects. - A generation without its own timestamp takes its step's, streamed from the same snapshot, only when its step/bot key belongs to one response; otherwise it is skipped with a warning, never dated by import time. Metadata blobs over 1 MiB are skipped.
- Quotas are off by default. With
AI_ACTIVITY_ANTIGRAVITY_QUOTAS=1in the environment the hooks run in, it collects measured five-hour and weekly quota snapshots using the signed-in Antigravity CLI's/usageJSON report. That runsagyautomatically, and each probe reaches Google's backend. Antigravity's terms forbid using third-party tools to access the service and allow suspending the account; the probe uses Google's own CLI and sign-in, but enable it at your own risk. Without it the card shows Unavailable quotas. Install agy 1.1.11 or later, sign in with the same Google account you use in Antigravity, and makeagyavailable on the collector's PATH (including hooks and scheduled tasks). Verifyagy --version(its output must contain the version) andagy -p /usage --output-format json --print-timeout 90sin a terminal. The CLI handles its own authentication; the collector never reads provider credential files. Desktop/IDE history still imports without the CLI; a missing CLI, an old or unrecognized version, a failed probe or an unsupported report leave quota windows Unavailable, with a diagnostic naming which. A windowagyreports untouched (100% left, resetting a full window length from now, or no reset time) has not started yet: it stays Unavailable rather than showing 0%. - Quotas refresh on hooks/manual/scheduled runs, at most once per minute
after a successful upload, including runs with no new token activity.
Failed probes or uploads back off for five minutes (or the server's
Retry-After), in~/.cache/ai-activity/antigravity-quota.json, apart from usage uploads: a refused quota upload never delays token imports. Attempts are saved before probing, so interruption cannot reset the throttle. Token collection finishes first; the probe then runs outside the collection lock, under its own lock, and a run that finds another probe in progress skips its own. Use the optional one-minute schedule above for updates while idle. Gemini and Claude/GPT pools remain separate: the card uses the same percentage bars, elapsed-window marks, reset countdowns and expiry behavior as Codex and Claude Code. Percentages are used quota, never inferred from tokens. Missing/disabled buckets stay unavailable; after reset, the old value stays unavailable until a fresh snapshot arrives. Free plans may expose only a weekly quota. Context fill remains unavailable. To preview the card, open/demoon your server (no account needed). That Demonstration data page includes fictional Antigravity quotas, activity and conversations; it never writes them to the server. - The quota subprocess runs
/usagein an empty temporary directory, without the Activity URL/key, and cannot recursively trigger this collector's hooks. Versions older than 1.1.11 or an unrecognized version never receive/usage, since older print modes may treat it as a prompt. LeaveAI_ACTIVITY_ANTIGRAVITY_QUOTASunset (or anything but1) to keep quota probing off.
Quota command/schema evidence comes from CodexBar's Antigravity implementation.
Google documents the quota command
and plan windows. Quota tests run
the collector against a fake agy with synthetic reports.
Format limitations: Antigravity's persisted protobuf layout is
undocumented. The parser follows independently observed field evidence,
plus the CLI 1.2.12 Gemini layout verified against a local stub (synthetic
usage 12/7, 31/11, then 37 input / 13 output / 4 cached / 6 thinking):
response id in 1.4.7 (or the request_id in chat.20 when the API returned
none), model in 1.19, uncached input in 1.4.2, cached input in 1.4.5,
thinking in 1.4.9, text output in 1.4.10.
It accepts standard protobuf generation timestamps, or a unique matching
step UUID (4) and response id with a standard step timestamp (steps.1).
Unknown timestamp layouts, missing response ids, corrupt records, and
ambiguous step matches are skipped with a diagnostic, and read again only
when their database changes. Rows whose model is not Gemini are skipped
too under the 1.2.12 rules (their field meanings are unverified), and rows
without a per-response id share their turn's request id and count once
(the largest response). They are never assigned the database
modification time or import time, so totals may be incomplete on
unsupported versions. Automated tests use synthetic SQLite/protobuf
fixtures for both the legacy and the 1.2.12 layouts.
- Create a device key as above (the same key serves every tool).
- Copy
collectors/cursor.pyto~/.cursor/hooks/ai-activity-cursor.pyand replace<server>and<device key>at its top (or setAI_ACTIVITY_URL/AI_ACTIVITY_KEYin Cursor's environment). - Merge this into the user
~/.cursor/hooks.json:
{
"version": 1,
"hooks": {
"afterAgentResponse": [
{"command": "python3 ~/.cursor/hooks/ai-activity-cursor.py --hook", "timeout": 10}
]
}
}On Windows, ~ is C:\Users\<user>. Use python and an absolute script
path in the command, with JSON-escaped backslashes and quotes:
{
"version": 1,
"hooks": {
"afterAgentResponse": [
{"command": "python \"C:\\Users\\<user>\\.cursor\\hooks\\ai-activity-cursor.py\" --hook", "timeout": 10}
]
}
}The one-command installer uses the absolute installing interpreter and a PowerShell wrapper on Windows, so spaced interpreter and user paths work. Prefer it if Python is not on Cursor's PATH. Check the hook in Cursor's Settings → Hooks / Hooks output channel; Cursor reloads the configuration on save (restart if it has not loaded).
What is measured. afterAgentResponse is the installed source of truth.
The collector also accepts a stop payload if wired manually; a repeated
conversation + generation ID counts once, with final counts replacing a
partial turn when output rises. Each dashboard call represents one measured
Agent turn, which can contain several model requests; the UI labels these
as "Agent turns". It requires all four
nonnegative integer counters: absent counts stay unavailable, never estimated.
Raw hook fields are input_tokens, output_tokens, cache_read_tokens,
cache_write_tokens (camelCase aliases are accepted). Hook input includes
cache reads and writes: the server stores input minus both caches, stores
cache separately, and retains output including reasoning. For input 1,000,
output 80, cache read 600 and cache write 100, the total is 1,080, with 300
uncached input. Cache sums above input or unsafe totals are rejected as unavailable,
rather than silently counting a changed counter convention. These are raw
hook counts, not the disjoint SDK usage shape.
The mapping was checked in the official
Cursor agent package 2026.09.28-64d2043:
the hook forwards turn usage and its Anthropic adapter subtracts both cache
counters from input. Hook configuration is described in
Cursor's hooks reference.
Privacy and retries. Before answering {}, the hook retains only
conversation/generation IDs, model, counts, timestamp and the local UTC
offset in ~/.cache/ai-activity/cursor.db (a private SQLite metrics journal,
using Python's standard library). It never retains or uploads reply/prompt
text, email, paths or provider keys. If a hook has no timestamp, live receipt
time is saved once; numeric epoch seconds/milliseconds and ISO timestamps
are accepted. Retries and partial/final updates preserve time and offset.
A detached worker uploads batches below the API's body-size limit, with
one uploader and at most one waiter. Indexed revisions select only new or
changed turns; idle runs never scan the retained history. Each of the eight
recent server/device targets keeps one checkpoint, which advances only
after acceptance, without a growing set of per-turn accepted hashes.
An HTTP 429 respects Retry-After (seconds or an HTTP date); other failed
uploads back off too. Retry on a later hook or run the copied script without
--hook; schedule that command if retries are needed while Cursor is idle.
Run it with --replay to resend retained history to the current target;
changing the URL/key also replays it. The journal keeps one compact row per
turn so history can be replayed; it grows with measured turns. Removing
cursor.db while the collector is stopped erases that local history.
Older cursor-events/*.json journals migrate once, preserving times and
local days, with a safe server-deduplicated replay. Invalid legacy files
are quarantined in cursor-invalid/; invalid rows cannot block other
metrics or upload unvalidated fields. A damaged SQLite database is kept
for recovery, never silently replaced with an empty one.
Scope and gaps. This installs user hooks for local IDE Agent Chat / Cmd+K only. It collects future hook observations, with no import of pre-install Cursor history. Tab completions are a different hook surface. Some CLI or non-interactive versions omit these hooks or token fields; cloud agents load project hooks and cannot read your user configuration. Neither is promised in this first version. Parent turn counts can omit subagent usage; no subagent tokens, quotas or context fill are inferred. Missing windows stay unavailable and the Cursor card shows activity, like OpenCode. See Cursor's explanation of the CLI hook gaps and parent-only token counts. Tests use synthetic hook fixtures; a real IDE turn remains to be verified.
- Create a device key as above (the same key serves every tool).
- Copy
collectors/opencode.pyto~/.config/opencode/ai-activity-opencode.pyand replace<server>and<device key>at its top (or setAI_ACTIVITY_URL/AI_ACTIVITY_KEYin the environment OpenCode runs in). - Copy
collectors/opencode-plugin.jsto~/.config/opencode/plugins/ai-activity.js. OpenCode loads it at start.
On Windows, ~ is your user directory (C:\Users\<user>), for OpenCode's
folders too, and the plugin runs python instead of python3: it must be
on OpenCode's PATH.
What it does:
- The plugin runs the collector, detached, when OpenCode starts and whenever a session goes idle (one run at a time).
- The collector reads OpenCode's own database
(
~/.local/share/opencode/opencode.db, read-only) and sends one entry per assistant message (keyed by itsmsg_…id) with its token counts, provider and model (stored asprovider/model) and the machine's UTC offset at that time. It selects numeric fields only: prompts, replies, tool output, titles and paths are never read. - Subagent sessions are sent as their root session, so a conversation with subagents counts once.
- OpenCode counts reasoning apart from output; it is added to output, like OpenCode's own totals, never twice.
- The first run sends the whole database: that is the import of past sessions.
- No 5-hour or weekly limit: OpenCode has none of its own, so the card shows the active conversations and today's usage instead.
- How far the database was sent is kept in
~/.cache/ai-activity/opencode.json, per server and device key (a new one gets the whole history), and only moves forward once the server accepted a batch, so nothing is lost while the server is down (the database is the queue). Delete that file to send everything again (safe: the server stores each message once). The script is idempotent: it can also run by hand or from cron.
Each tool has one Python script in collectors/. They share the same design:
- One file, standard library only. Copy it next to the tool, set
<server>and<device key>at its top, orAI_ACTIVITY_URL/AI_ACTIVITY_KEYin the tool's environment (the environment wins). Linux, macOS and Windows alike (python3orpython). - Metrics only. They read local usage files read-only or receive live hook metrics, and send ids,
model, time, the machine's UTC offset and token counts (plus quotas and
context fill where the tool has them, and what a message's API-equivalent
price depends on: Claude Code's cache write durations,
speed,service_tierandinference_geo; Codex's service tier and model provider). Prompts, replies, tool output, titles, paths and provider keys never leave the device. - The key only goes to your server. Uploads never follow an HTTP redirect: a redirect fails the run (progress unchanged) instead of sending the device key somewhere else.
- Progress only moves on success. How far each source was sent is kept
under
~/.cache/ai-activity/(JSON offsets, or Cursor's SQLite checkpoints), saved once the server accepted it. While the server is down nothing moves: the next run sends the backlog with its original times. Delete JSON offsets, or run Cursor with--replay, to resend history; the server stores each message once, and a message seen again with more output tokens replaces its partial counts. - Progress is per server and key. The progress store keeps one set of
offsets per server URL and device key (JSON
{"targets": {"<fingerprint>": …}}, or Cursor'stargetstable; the fingerprint is the first 16 hex digits of the SHA-256 of both, never the key itself), for the 8 most recently used. Point a device at a new server, or give it a new key, and its next run sends the whole local history there; switch back and it resumes where it was. With a new key on the same account, everything comes back as already stored. With another account on the same server, messages the first account already sent stay with it (the server never moves them between accounts). Claude Code's and Codex's progress also records which message fields it was sent with ("fields"): a collector that sends new fields sends its history once more, and the server fills them in on messages it stored without them (nothing is counted twice). - Never in the tool's way. Called from a hook or the status line, a script answers at once and uploads from a detached copy of itself (Linux/macOS: its own session; Windows: out of the console, the process group and, when permitted, the parent job). A failure prints one line on stderr and exits 1; the tool is never blocked, and the next run retries.
- One upload at a time. A lock file in
~/.cache/ai-activity/(flock; on Windowsmsvcrton its first byte) serializes runs. Where hooks fire often (Claude Code, Codex, Antigravity), one more run may wait behind the active one and any other exits at once: the waiter reads the sources only once its turn comes, so it sends what they would have. - Idempotent. Any script can also run by hand or from cron (Task Scheduler on Windows): it sends only what is new.
- Versioned. Each script has a
VERSIONat its top and sends it with every upload. When the server has a newer one, the script says so on stderr and in~/.cache/ai-activity/update-available-<tool>(removed once up to date), and Settings → Devices flags that device's collector as outdated. To update, run the device's install command again (or copy the new script by hand). The server never sends code: updates are always yours to run. A collector too old for the server (426) keeps its backlog, which goes out once it is updated.
| Script | Copy to | Run by | Reads | Progress file | Locks |
|---|---|---|---|---|---|
claude-code.py |
~/.claude/ai-activity-claude-code.py (Windows status wrapper: .ps1 next to it) |
the UserPromptSubmit, PostToolUse, Stop, StopFailure and SessionEnd hooks (tokens); the statusLine, every refresh (quotas, context) |
~/.claude/projects/**/*.jsonl (sessions and subagents) |
offsets.json (byte offset per transcript), status.json (posted/pending status and recent contexts per target) |
lock, waiter.lock, status-state.lock, status-<target>.lock |
codex.py |
~/.codex/ai-activity-codex.py |
the UserPromptSubmit, PostToolUse, Stop and SessionEnd hooks |
~/.codex/sessions, ~/.codex/archived_sessions (CODEX_HOME) |
codex.json (byte offset per rollout) |
codex.lock, codex-waiter.lock |
cursor.py |
~/.cursor/hooks/ai-activity-cursor.py (Windows: .ps1 next to it) |
afterAgentResponse user hook |
stdin metrics, then cursor.db |
cursor.db (metrics and one revision checkpoint per target; --replay resets the current target) |
SQLite transactions, cursor.lock, cursor-waiter.lock |
opencode.py |
~/.config/opencode/ai-activity-opencode.py |
opencode-plugin.js, at start and on session.idle |
~/.local/share/opencode/opencode.db (XDG_DATA_HOME, OPENCODE_DB), numeric fields only |
opencode.json (last time_updated sent) |
opencode.lock |
antigravity.py |
~/.gemini/ai-activity-antigravity.py |
the PostInvocation and Stop hooks |
~/.gemini/{antigravity,antigravity-cli,antigravity-ide}/conversations/*.db (GEMINI_CLI_HOME); quotas from agy, opt-in |
antigravity.json (per database), antigravity-quota.json |
antigravity.lock, antigravity-waiter.lock, antigravity-quota.lock |
How each one is started:
claude-code.py:--hook(the hooks) reads the hook's JSON on stdin (never sent), startsclaude-code.py --workerdetached, and prints nothing. Without arguments (the statusLine), it hands the status line's JSON toclaude-code.py --worker --status, which posts the quotas and context fill.--workeralone collects the tokens in the foreground: runpython3 ~/.claude/ai-activity-claude-code.py --worker(on Windows,python "<path>" --worker) to see errors while setting up.codex.py: without arguments, collects in the foreground (by hand, cron, and the Linux hooks, which detach it withsetsid -f).--hook(the macOS and Windows hooks) prints{}for Codex and starts the script again detached.cursor.py:--hookallowlists stdin metrics into its local journal, prints{}, and starts itself detached. Without arguments it uploads unaccepted journal entries in the foreground (also suitable for cron).opencode.py: without arguments, collects in the foreground; the plugin starts it detached, one run at a time.antigravity.py: without arguments, collects in the foreground and prints diagnostics (skipped rows or databases, quota availability).--post-invocationand--hookanswer the hook ({}, or{"decision":"stop"}forStop) and start a detached worker, which waits 2 seconds for Antigravity to write its metadata.
Claude Code, Codex, Cursor and OpenCode runs give up after 15 minutes (the progress
already accepted is kept). The payloads each script sends are described in
AGENTS.md §5.
npm test runs the installed collector integration tests on Linux and ARM64;
the Windows and macOS CI jobs run them explicitly. Each test starts a temporary app and
local HTTP receiver, runs the served installer in an isolated home, invokes
the installed status line, hook or plugin against synthetic tool data, then
checks the upload format, retry, deduplication and dashboard totals. No model
API or external account is needed.
The Collector CLI smoke workflow runs on relevant collector changes, on
pull requests and pushes to main, every Monday against the latest CLI
releases, and can be started manually from Actions (pinned or latest
versions). Its Linux x64, Linux ARM64, Windows x64 and macOS jobs install the
Claude Code, Codex and OpenCode CLIs, point each at a local fake model API, complete one
chat, and check that the installed integration reaches the real app. Windows
uses a temporary ConPTY for Claude Code's interactive status line; macOS uses
a pseudo-terminal through node-pty for the same check and exercises Codex without
setsid. The same jobs also install the latest Antigravity CLI, chat with a
local Gemini stub, and verify that its installed hook uploads measured usage; see the
Antigravity section above. agy stays in its own steps because it is a
native binary rather than a Node CLI: it cannot be pinned with the rest and
intentionally tracks the latest release, and its chat is headless (agy -p
print mode, no TUI). No model API key or external account is needed. This
path-filtered workflow is not a required check on unrelated PRs. Each run's
summary lists, per platform, the CLI version tested and the result per tool;
a failed weekly run opens or updates an issue naming the failing tool and
platform.