Aggregate ccusage across every machine where you run Claude Code — one merged view of tokens, cost, and active billing blocks, over SSH.
┌────────────┬──────────────────────┬─────────┬─────────┬───────────┬─────────────┬─────────────┬─────────┬──────────────────────┐
│ Date │ Models │ Input │ Output │ Cache+ │ CacheR │ Total │ Cost │ Devices │
├────────────┼──────────────────────┼─────────┼─────────┼───────────┼─────────────┼─────────────┼─────────┼──────────────────────┤
│ 2026-04-10 │ haiku-4-5, opus-4-6 │ 11,825 │ 558,993 │ 2,791,514 │ 113,085,015 │ 116,447,347 │ $87.15 │ local, zju-pro6000 │
│ 2026-04-11 │ opus-4-6, sonnet-4-6 │ 2,074 │ 487,914 │ 2,324,279 │ 55,606,452 │ 58,420,719 │ $54.47 │ local, zju-pro6000 │
│ TOTAL │ … │ 372,737 │ … │ … │ … │ … │ $308.61 │ local, zju-pro6000 │
└────────────┴──────────────────────┴─────────┴─────────┴───────────┴─────────────┴─────────────┴─────────┴──────────────────────┘
Think of it as "ccusage, but across all your dev boxes."
If you run Claude Code on more than one machine — laptop, workstation, GPU boxes, remote dev hosts — ccusage only sees one at a time. You end up SSH-ing around, running ccusage daily on each, and adding the numbers in your head. ccfleet does that for you: one command, one merged table, no data ever leaves the hosts unencrypted.
npm install -g ccfleetOr run ad-hoc without installing:
npx ccfleet dailyNo config file to write. ccfleet builds the config for you.
npm install -g ccfleet
ccfleet devices add local --local
ccfleet dailyThree commands. You'll see a merged table of every day you've used Claude Code on this machine.
This step only applies if you already use ssh to connect to another computer (a server, a GPU box, a home desktop, etc). If you only run Claude Code on this one machine, Step 1 is all you need — skip to Step 3.
🍎 macOS users: install GNU rsync first.
brew install rsyncApple ships
openrsync2.6.9, which crashes withrenameat: No such file or directorywhenever Claude Code is actively writing on the remote while ccfleet is mirroring it. GNU rsync 3.x (~10 seconds to install via Homebrew) fixes this and ccfleet picks it up automatically. Skip this only if you plan to use--cache-mode directeverywhere — but cache mode is much faster and usually what you want.
What's an "ssh alias"? When you type a command like ssh gpu-box and it connects somewhere, gpu-box is an alias defined in your ~/.ssh/config file. ccfleet reuses whatever aliases you already have — it does not ask for passwords and does not set up ssh for you.
Option A — you already ssh into it with a short name.
Suppose you normally connect with ssh gpu-box. Then:
# Use YOUR alias here, not "gpu-box" — that's just an example.
ccfleet devices add gpu --ssh-alias gpu-box --cache-mode cache
ccfleet devices testgpu is the label ccfleet shows in tables; gpu-box is the actual ssh alias it connects with. You can use the same word for both if you want.
⚠️ If you seeunknown hostorCould not resolve hostname, the alias you passed does not exist in your~/.ssh/config. Tryssh gpu-boxyourself first — if that fails from your terminal, ccfleet can't connect either. Fix your~/.ssh/config, or use Option B below.
Option B — you don't have an ssh alias, just a hostname + user.
ccfleet devices add gpu \
--host 192.168.1.42 \
--user your_username \
--port 22 \
--identity ~/.ssh/id_ed25519 \
--cache-mode cache
ccfleet devices testReplace 192.168.1.42, your_username, and the identity-file path with whatever you'd actually type into ssh. --port 22 and --identity … are optional — omit them if you don't need them.
Once devices test says ok:
ccfleet daily # merged across laptop + gpuccfleet daily --since 20260401 # just April
ccfleet blocks --active # current 5-hour billing block
ccfleet session --merge # grand total across everything
ccfleet daily --group-by device # break down per machine
ccfleet daily --currency CNY # show cost in ¥ instead of $
ccfleet devices list # what's configuredccfleet devices add writes ~/.config/ccfleet/config.toml for you with sensible defaults. You never need to edit TOML. If you want to, the schema is documented in Configuration reference and an annotated example lives in examples/config.toml.
- Node 20+ on the machine running ccfleet.
- On each remote, one of:
- Direct mode:
ccusagereachable (default isnpx -y ccusage@latest, override withccusage_cmd). - Cache mode:
rsyncreachable, plusccusageinstalled locally (once, on the ccfleet machine) — it runs against the mirrored JSONL.
- Direct mode:
- macOS users, please install GNU rsync:
brew install rsync. Apple's bundledopenrsync2.6.9 has known rename-race bugs when the remote~/.claude/projectsis being actively written by Claude Code, and you will hit them.
| Command | What it does |
|---|---|
ccfleet daily |
Per-day token/cost table merged across devices. |
ccfleet monthly |
Per-month rollup. |
ccfleet session |
Per-session rollup. |
ccfleet blocks |
ccusage's 5-hour billing-block view. --active shows the current block. |
ccfleet sync |
Force an rsync for all cache-mode devices. --status shows last-sync-at + bytes. |
ccfleet devices list|test|add|remove |
Device management. |
ccfleet run <device> -- <args…> |
Escape hatch — run ccusage <args…> on a specific device, raw stdout. |
All query commands support:
| Flag | |
|---|---|
--since 20260401 --until 20260411 |
Date range (forwarded to ccusage). |
--timezone America/Los_Angeles |
Time-zone for the day boundary. |
--group-by device / --group-by project |
Slice the merged table. |
--merge |
Single grand-total row. |
--device <name> (repeatable) |
Limit to specific hosts. |
--breakdown |
Per-model rows. |
--json |
Machine-readable output. |
--refresh |
Force an rsync before reading. |
--direct |
Bypass the cache for this query. |
--currency USD|CNY|SGD|EUR|… |
Show cost in any ISO 4217 currency. Live ECB rates, refreshed once per day and cached locally. --json output always stays in USD. |
Every device is queried in one of two modes:
- Direct mode —
ssh <host> -- npx -y ccusage@latest daily --json, parse the response. Zero local state. Needs ccusage reachable on the remote. - Cache mode —
rsyncthe remote~/.claude/projectsto~/.cache/ccfleet/<device>/projects, then run ccusage locally against the mirror withCLAUDE_CONFIG_DIRpointed at the cache dir. Warm queries are effectively free because ccusage is reading local JSONL, and the same mirror answersdaily,monthly,session, andblockswithout re-syncing.
Cache mode auto-refreshes when the mirror is older than cache_ttl_minutes (default 15). Use --refresh to force a sync mid-session, --direct to skip the cache entirely. local devices always run ccusage locally, uncached.
All device queries run in parallel, bounded by defaults.parallel. A failure on one device never blocks the others — it surfaces as a per-device error row with an actionable hint ("unknown host", "host key verification failed", "remote missing ccusage", etc).
[defaults]
parallel = 4 # max concurrent device queries
timeout_seconds = 30 # per-device timeout
cache_mode = "direct" # "direct" (default) | "cache"
cache_ttl_minutes = 15 # auto-refresh threshold for cache mode
claude_dir = "~/.claude" # remote Claude data dir (override per-device)
ssh_multiplex = true # ControlMaster/ControlPersist for speed
currency = "USD" # 3-letter ISO (USD|CNY|SGD|EUR|GBP|JPY|…); `--currency` overrides
[[devices]]
name = "work" # required, unique
# Pick one of three ways to address the host:
ssh_alias = "work" # (a) use ~/.ssh/config — recommended
# host = "work.example.com" # (b) raw hostname + optional:
# user = "alice"
# port = 22
# identity = "~/.ssh/id_ed25519"
# local = true # (c) mark as local, no ssh, no cache
cache_mode = "cache" # override defaults per-device
claude_dir = "/home/alice/.claude"
ccusage_cmd = "/home/alice/.bun/bin/ccusage" # replace the default npx invocationOr manage devices from the CLI:
ccfleet devices add work --ssh-alias work --cache-mode cache
ccfleet devices add gpu --host gpu.example.com --user alice --identity ~/.ssh/id_ed25519
ccfleet devices test
ccfleet devices remove gpuunknown host (check ~/.ssh/config) / Could not resolve hostname … — the --ssh-alias you passed isn't defined in your ~/.ssh/config, or it points at a hostname DNS can't resolve. Quick check: run ssh <that-alias> yourself in a new terminal. If that fails, ccfleet can't connect either — it's a plain ssh problem, not a ccfleet problem. Fix your ssh config, or re-add the device with --host <ip-or-hostname> / --user … flags instead of --ssh-alias.
command not found: ccusage — the remote doesn't have ccusage. Install it (npm i -g ccusage), or set ccusage_cmd to a pinned path. Direct mode defaults to npx -y ccusage@latest, which requires node + npm on the remote.
rsync failed (exit 10/23/24): renameat: No such file or directory — you're on macOS's openrsync 2.6.9. brew install rsync to get GNU rsync 3.x, which is first on PATH after install.
permission denied (publickey) — run ssh <alias> manually once to confirm your key is loaded and the remote accepts it. ccfleet uses BatchMode=yes, so it can't prompt for passwords interactively.
host key verification failed — ssh <alias> once to accept the host key, then retry.
Numbers don't match what ccusage shows locally — ccfleet run <device> -- daily --since YYYYMMDD runs ccusage on that device raw, so you can diff against the merged view and find the drift.
Want to see what ccfleet is actually running? — DEBUG=1 ccfleet daily prints stack traces on errors. For the literal argv, ccfleet run <device> -- --help is the passthrough.
git clone https://github.com/tangshunpu/ccfleet
cd ccfleet
npm install
npm run build # produces dist/cli.js
npm link # puts `ccfleet` on your PATH, pointing at dist/cli.jsArchitecture notes and future work live in PLAN.md.
MIT