Everyone feels 10× more productive with AI. Almost nobody measures it. Here's the exact setup I use to track agent leverage as a live KPI — how many hours my agents work for every hour I put in. Mine moves week to week — recomputed straight from the logs every time, currently around 7:1.
The whole thing reads from data you already have: your Claude Code session logs.
Claude Code writes a full transcript of every session as JSONL — one event per line, every event timestamped:
ls ~/.claude/projects/*/*.jsonl | headEach project folder holds its sessions. That timestamp stream is all you need — no extra tooling, no AI estimating anything.
- Agent-active hours = wall-clock the agent actually spent working — from the moment you send a real message to the moment you send your next one, capped per turn (I use 15 min) so a session you walked away from doesn't inflate it.
- Your input hours = your real hands-on time (prompting, reviewing, deciding). Track it honestly — and exclude anything that isn't this work (for me: study time).
Leverage = agent-active hours ÷ your input hours. If you can't defend both numbers, the ratio is a vanity metric. The cap + the exclusions are what make it honest.
The one trap that quietly breaks this: Claude Code logs tool results (the output of a
file read, a bash command, etc.) as type: "user" events too — they're not something you
typed. If you count every "user" event as a real message, you reset your "turn" dozens of
times during a single burst of agent work and end up measuring almost nothing. The fix: a
real message from you has message.content as a plain string; a tool-result "user"
event has message.content as an array of tool-result blocks. Only the string ones count.
Drop this in leverage.mjs and run node leverage.mjs:
import { readFileSync, readdirSync, statSync } from "node:fs";
import { join } from "node:path";
// Plain recursive walk — no fs.globSync (Node 22+ only, still experimental).
// This runs on any reasonably modern Node.
function findJsonl(dir) {
let out = [];
for (const name of readdirSync(dir)) {
const p = join(dir, name);
const st = statSync(p);
if (st.isDirectory()) out = out.concat(findJsonl(p));
else if (name.endsWith(".jsonl")) out.push(p);
}
return out;
}
const CAP_MS = 15 * 60 * 1000; // cap per turn so idle gaps don't inflate
const files = findJsonl(`${process.env.HOME}/.claude/projects`);
let agentMs = 0;
for (const f of files) {
const lines = readFileSync(f, "utf8").trim().split("\n").filter(Boolean);
let turnStart = null, turnEnd = null;
for (const line of lines) {
let e;
try { e = JSON.parse(line); } catch { continue; } // skip any malformed line
const t = new Date(e.timestamp).getTime();
if (Number.isNaN(t)) continue;
// Real message from you = type "user" AND content is a plain string.
// Tool results are also type "user" but content is an array — not a real turn.
const isRealUserMessage = e.type === "user" && typeof e.message?.content === "string";
if (isRealUserMessage) {
if (turnStart !== null) agentMs += Math.min(turnEnd - turnStart, CAP_MS);
turnStart = t; turnEnd = t; // start the next turn
} else if (turnStart !== null) {
turnEnd = t; // extend the current turn
}
}
if (turnStart !== null) agentMs += Math.min(turnEnd - turnStart, CAP_MS);
}
const agentHours = agentMs / 3.6e6;
const myHours = Number(process.argv[2] ?? 14); // your tracked input hours this week
console.log(`agents: ${agentHours.toFixed(1)}h · me: ${myHours}h · leverage ${(agentHours / myHours).toFixed(1)} : 1`);node leverage.mjs 14 # 14 = your input hours this week
# → agents: 98.0h · me: 14h · leverage 7.0 : 1That's the number. Measured, not guessed.
A lifetime ratio is meaningless. Filter events to the last 7 days so the KPI moves and stays comparable week to week:
const WEEK_AGO = Date.now() - 7 * 864e5;
// inside the loop: if (t < WEEK_AGO) continue;Run it on a cron (or a Claude Code /leverage skill) so it recomputes on its own.
Write the result to a tiny JSON your site can read, and recompute on a schedule:
import { writeFileSync } from "node:fs";
writeFileSync("leverage.json", JSON.stringify({ ratio: +(agentHours/myHours).toFixed(1), agentHours, myHours, ts: Date.now() }));Point a small widget at leverage.json. Now the number on your page moves every time you
work — it's a screenshot nobody can fake, because it isn't a screenshot.
- The cap is a judgment call — pick one and keep it fixed.
- "Agent-active" ≠ "productive". Lots of agent-hours on the wrong thing is still wasted time. Pair this with an outcome metric (shipped features, not just hours).
- Your input hours are self-reported. The exclusions (study, dabbling) are what keep you honest.
- Double-check the tool-result trap above — it's the single easiest way to quietly get this number wrong in either direction.
This tracker measures your leverage. The reason mine keeps climbing is the setup behind it — a CLAUDE.md + skills + memory system that makes Claude remember your whole project and run like a senior dev instead of forgetting everything every session. That's the Operator Setup: https://get.caffier-agency.com
New to this? Start free with the 7-Day Operator Challenge (same link) — it builds the foundation. The live leverage-tracking above is part of the paid guide, not the free challenge.