← Back to Cookbook

Track Claude Code token consumption per project with the Recorder (Stop hook)

A Stop hook aggregates Claude Code's session JSONL at end-of-session and POSTs Input / Output / CacheRead / CacheCreate totals to the Recorder. Each launch directory maps to a metric prefix you choose — align it with `System/Code/<app>/` and your Claude token spend lines up next to your DORA / Code series, a per-app time series the Anthropic Console can't give you.

What this recipe records

A Claude Code Stop hook fires at end of session, aggregates the JSONL transcript, and POSTs 4 values to the Recorder under a <MyPrefix> you choose:

  • <MyPrefix>/Input — input tokens (raw prompt)
  • <MyPrefix>/Output — output tokens
  • <MyPrefix>/CacheRead — tokens served from the prompt cache
  • <MyPrefix>/CacheCreate — tokens written to the prompt cache

Name one <MyPrefix> per Claude Code launch directory. Aligning it with System/Code/<app>/Claude/Tokens puts your token spend in the same tree as the Lines and Deploy series from the DORA recipe, so you can overlay "LOC growth vs token spend" or "deploy count vs token spend" in one chart — slices the Anthropic Console (API-key-scoped Usage) can't give you.

Prerequisites

Why a hook, not a cron

Claude Code session transcripts live only on your machine — ~/.claude/projects/<encoded-cwd>/<session-id>.jsonl. Each line carries message.usage = {input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens} with a timestamp. A GitHub Actions cron can't reach this data; running locally at session-end is the natural fit.

Setup

1. Mint a Recorder PAT

In Settings → API Tokens, create a PAT, copy the string immediately (shown only once), and export RECORDER_PAT="..." from your ~/.zshrc or equivalent.

2. Check the directory you want to track

Claude Code names project dirs by /--encoding your launch cwd. Launch Claude Code once from the dir you want to track, then ls the projects directory and copy the matching basename:

ls ~/.claude/projects/
# e.g. -Users-me-code-myapps   ← copy this line

3. Drop in the Stop hook script with your dir and metric prefix

Save as ~/bin/post_claude_tokens.sh. Only the two lines in the case arm need to change — leave the rest alone.

#!/usr/bin/env bash
set -euo pipefail
 
: "${RECORDER_PAT:?env RECORDER_PAT is not set}"
 
PAYLOAD=$(cat)
TRANSCRIPT=$(echo "$PAYLOAD" | jq -r '.transcript_path')
PROJECT_BASENAME=$(basename "$(dirname "$TRANSCRIPT")")
 
# ─── ⬇ Edit these two lines for your setup ⬇ ───
case "$PROJECT_BASENAME" in
  -Users-me-code-myapps)                                # ← the basename from step 2
    METRIC_PREFIX="System/Code/myapps/Claude/Tokens" ;; # the prefix you want in Recorder
  *)
    exit 0 ;;
esac
# ─── ⬆ end of edits ⬆ ───
 
read -r INPUT OUTPUT CREAD CCREATE <<<"$(jq -s -r '
  reduce .[] as $e (
    {input:0, output:0, cread:0, ccreate:0};
    .input    += ($e.message.usage.input_tokens // 0)
    | .output   += ($e.message.usage.output_tokens // 0)
    | .cread    += ($e.message.usage.cache_read_input_tokens // 0)
    | .ccreate  += ($e.message.usage.cache_creation_input_tokens // 0)
  ) | "\(.input) \(.output) \(.cread) \(.ccreate)"
' "$TRANSCRIPT")"
 
post() {
  local name=$1 value=$2
  curl -sS -X POST https://api.recorder.nasebanal.com/api/v1/records \
    -H "Authorization: Bearer $RECORDER_PAT" \
    -H "Content-Type: application/json" \
    -d "{\"name\":\"$name\",\"unit\":\"tokens\",\"data_type\":\"integer\",\"chart_type\":\"line\",\"value\":$value}"
}
 
post "$METRIC_PREFIX/Input"       "$INPUT"
post "$METRIC_PREFIX/Output"      "$OUTPUT"
post "$METRIC_PREFIX/CacheRead"   "$CREAD"
post "$METRIC_PREFIX/CacheCreate" "$CCREATE"

Make it executable:

chmod +x ~/bin/post_claude_tokens.sh

To track more than one directory, add another arm to the case block.

4. Register the Stop hook in Claude Code settings

~/.claude/settings.json:

{
  "hooks": {
    "Stop": [
      {
        "matcher": "",
        "hooks": [
          {
            "type": "command",
            "command": "bash ~/bin/post_claude_tokens.sh"
          }
        ]
      }
    ]
  }
}

5. Smoke test

Open one short Claude Code session from the dir you noted in step 2 and end it cleanly. The Recorder tag list will show <MyPrefix>/{Input,Output,CacheRead,CacheCreate} — one data point per session, four tags growing in parallel from there.

⚠️ Secret handling

  • Keep RECORDER_PAT in ~/.zshrc or your secret manager — never hardcode it in the script
  • If you suspect a leak, revoke the PAT in Recorder Settings → API Tokens, mint a new one, and overwrite the environment variable
  • The Stop hook fires for every Claude Code session. Projects not listed in the case map are silently dropped by the *) exit 0 arm, so untracked work never bleeds into the Recorder