Track billable hours across Claude Code, Codex CLI, and OpenCode — with git commit history as independent, tamper-evident evidence — and generate client-ready hour reports.
If you bill clients hourly for AI-assisted development work, self-reported timers aren't very convincing and git log only shows commit moments, not how long you actually worked. Commitly listens to each tool's session lifecycle hooks, logs activity locally to SQLite, derives real work sessions from that activity, cross-references matching git commits, and exports CSV/HTML reports (or a live dashboard) you can hand to a client.
commitly initregisters a lightweight hook in whichever of Claude Code, Codex CLI, and OpenCode are installed on your machine. Each hook just pipes an event tocommitly hook <tool> <event>— all real logic stays in this codebase, not scattered shell scripts.- Every hook invocation appends a raw event (tool, session id, cwd, timestamp) to a local SQLite database (
~/.commitly/commitly.db). Nothing is paired or interpreted at write time. - At report time, Commitly derives "work sessions" from that raw event stream using an idle-gap algorithm: events belonging to the same session id are grouped, and a session is considered to end either when an authoritative close event fires (Claude Code's
SessionEnd, OpenCode'ssession.deleted) or when the gap to the next event exceeds a configurable idle threshold (default 15 minutes). This uniform algorithm is necessary because only Claude Code has a clean session-close signal — Codex CLI has no dedicated close event at all, and OpenCode's closest equivalent isn't guaranteed to fire on abrupt exit. - Commitly looks up git commits made in each session's repo during that time window and attaches them as corroborating evidence.
commitly reportexports CSV or a self-contained, print-to-PDF-friendly HTML file.commitly dashboardruns a local web UI over the same data.
bun install
bun link # makes `commitly` resolvable on $PATH
commitly initWithout bun link, commitly init still works from a checkout — it falls back to invoking bun <path-to-cli.ts> directly in the registered hooks.
commitly init is non-destructive: it detects which tools are installed, and merges its own hook entries into each tool's existing config without touching any other hooks already registered there (verified against real-world configs with other tools' hooks already present). Re-running it is idempotent.
commitly init # register hooks for every detected tool, user scope
commitly init --tools claude-code # only Claude Code
commitly init --scope project # register in ./.claude, ./.codex, ./.opencode instead of $HOME
commitly init --force # replace an existing commitly hook entry (e.g. after upgrading)commitly report # this month, CSV to stdout
commitly report --from 2026-06-01 --to 2026-06-30 --format html --out june.html
commitly report --project acme --client "Acme Corp"
commitly report --with-git=false # skip git commit lookups (faster, no evidence column)
commitly dashboard # local web UI at http://localhost:4317
commitly doctor # verify hooks are registered and the DB is healthy
commitly projects add --key acme --name "Acme Website" --client "Acme Corp" --rate 150 --path-prefix /path/to/acme
commitly projects listProjects are optional — an unconfigured repo still shows up in reports, using its folder name, just without a rate or client attached. Configure commitly projects add for anything you actually bill.
~/.commitly/commitly.db— SQLite database, append-only raw events plus a regenerable session cache.~/.commitly/projects.json— human-editable project/client/rate registry. Matched by path prefix (longest match wins) or git remote URL.~/.commitly/config.json—idleGapMinutes(default 15) anddefaultCurrency.~/.commitly/hook-errors.log— hook normalization failures are logged here rather than ever blocking your actual coding session; checkcommitly doctorif a session seems to be missing from a report.
- Claude Code: hooks live in
.claude/settings.json. Commitly registersSessionStart,Stop, andSessionEnd.Stop(fires after every turn) is required, not optional — without it, a long but continuous session with no other activity looks identical to two isolated moments far apart in time, and gets incorrectly split by the idle-gap algorithm. - Codex CLI: hooks live in
hooks.json. Commitly registersSessionStartandStop. Codex has no session-close event at all, so every Codex session boundary is decided by the idle-gap heuristic. As of Codex 0.120 there's a known bug where project-local hooks sometimes don't fire in interactive sessions — user-scope (the default) is recommended. - OpenCode: Commitly writes a generated plugin file to
~/.config/opencode/plugins/commitly.js, subscribed tosession.created,session.idle, andsession.deleted.session.deletedis treated as an authoritative close, same tier as Claude Code'sSessionEnd, though it isn't guaranteed to fire on an abrupt exit.
bun install
bun test # unit + integration tests, including a real scratch git repo for git evidence
bunx tsc --noEmit
bun run src/cli.ts <command>MIT