Skip to content

Token Usage ​

kendo shows how many AI tokens went into each issue, and adds them up per sprint and per epic. The numbers come from your agents: each session reports its own usage when it ends. Nobody types them in.

Three ways send usage, and all three land in the same totals:

WayUse it when
CLI: kendo tokens logA hook or script knows the counts. The Claude Code hook below uses it
MCP: record-token-usageYour harness measures the counts and talks to kendo over MCP
API: POST /api/projects/{id}/token-eventsYou build your own integration

What Gets Counted ​

Each event carries one session's counts for one model: input tokens, output tokens, cache reads and cache writes. An issue's total is the sum of all four over its events. kendo stores counts only, never prompts or responses.

A session that used two models, such as Opus for the main agent and Haiku for its subagents, sends two events.

Each event carries an id made of the session id and the model. Sending the same session again replaces its totals instead of adding them, so a retry or a resumed session never counts twice.

Who Can Send ​

Every member of the project can send usage, as long as their role has Token Events: Create. The Member role has it. A role created after 2026-10-08 starts without it: an admin turns it on at the Roles page. Admins and the project owner can always send.

Claude Code: Send Every Session ​

This hook runs when a Claude Code session ends. It reads the session's exact usage from its transcript, takes the issue key from the git branch name, and calls kendo tokens log once per model.

You need:

  • the kendo CLI, logged in (kendo auth login), with the issue's project selected (kendo project select)
  • jq and git
  • branches that start with the issue key, such as KD-0042-fix-login

1. Save the Hook Script ​

Save this as ~/.claude/hooks/kendo-tokens.sh and make it executable (chmod +x ~/.claude/hooks/kendo-tokens.sh):

bash
#!/usr/bin/env bash
# Claude Code SessionEnd hook: send this session's token usage to kendo, one call per model.
set -uo pipefail

input=$(cat)
session=$(jq -r '.session_id' <<<"$input")
transcript=$(jq -r '.transcript_path' <<<"$input")
cwd=$(jq -r '.cwd' <<<"$input")

# The issue key from the branch name: KD-0042-fix-login gives KD-0042.
key=$(git -C "$cwd" branch --show-current 2>/dev/null | grep -oE '^[A-Za-z0-9]+-[0-9]+')
[ -n "$key" ] || exit 0

# Subagents write their own transcripts in a folder named after the session.
shopt -s nullglob
for file in "$transcript" "${transcript%.jsonl}"/subagents/*.jsonl; do
  jq -cR 'fromjson? | select(.type == "assistant" and .message.usage != null and .message.model != "<synthetic>")
    | .message | [.id, .model, (.usage | .input_tokens // 0, .output_tokens // 0, .cache_read_input_tokens // 0, .cache_creation_input_tokens // 0)]' "$file"
done | jq -nc '
  # One entry per reply id, the largest value of each count: a reply spans several lines.
  reduce inputs as [$id, $model, $in, $out, $read, $write] ({};
    .[$id] |= {model: $model, in: ([.in, $in] | max), out: ([.out, $out] | max), read: ([.read, $read] | max), write: ([.write, $write] | max)})
  | reduce .[] as $r ({};
    .[$r.model] |= {model: $r.model, in: (.in + $r.in), out: (.out + $r.out), read: (.read + $r.read), write: (.write + $r.write)})
  | .[]' |
while read -r row; do
  model=$(jq -r '.model' <<<"$row")
  kendo tokens log "$key" --model "$model" \
    --in "$(jq '.in' <<<"$row")" --out "$(jq '.out' <<<"$row")" \
    --cache-read "$(jq '.read' <<<"$row")" --cache-creation "$(jq '.write' <<<"$row")" \
    --id "$session:$model"
done

exit 0

Claude Code writes one reply to the transcript as several lines with the same message id, so the script counts each id once, with the largest value of each count. It skips Claude Code's own <synthetic> messages, which carry no usage. It streams the transcript and keeps one small entry per reply: a 32 MB transcript took about 8 MB of memory.

2. Register the Hook ​

Add the hook to ~/.claude/settings.json (every project) or .claude/settings.json (one repository):

json
{
  "hooks": {
    "SessionEnd": [
      {
        "hooks": [
          {"type": "command", "command": "~/.claude/hooks/kendo-tokens.sh"}
        ]
      }
    ]
  }
}

3. Check the Result ​

End a session on an issue branch, then run kendo issue view KD-0042. The AI tokens: line shows the issue's total. The issue page, the sprint list and the epic show it too.

Limits ​

  • A session on a branch without an issue key sends nothing. The script exits quietly.
  • The issue must be in the CLI's selected project. kendo tokens log looks the key up in that project, and fails for an issue in another one.
  • claude --resume keeps the session id, so the resumed session's larger total replaces the earlier one. --fork-session starts a new id, so a forked session counts its copied history again.
  • A failed call is not retried. The script always exits 0, so it never blocks Claude Code. When a resumed session ends, it sends the session's full total again.

Other Agents ​

Any agent that knows its counts can send them. Call kendo tokens log from its own end-of-run hook, use the MCP tool, or post to the Token Events API. Give every session and model its own external_event_id, so a resend replaces the totals instead of adding them.