Skip to content

Claude Time: How Many Hours Do I Actually Spend in Claude Code?

Published on
Reading time
9 mins read

Claude Code is open on my Mac most days, sometimes in several projects at once. I wanted to know how much of my day actually goes into it, and into which project, and I couldn't say. Session length doesn't tell you: a session stays open through lunch, and two sessions side by side count the same hour twice.

So I built Claude Time, a macOS menu bar app and CLI that answers the question from the transcripts Claude Code already writes. It's the same reason I wrote a disk report for my Coolify server: if a tool doesn't show the number you care about, build the report. It's MIT-licensed, and version 0.1.0 is out.

What it shows

The menu bar shows today's time in Claude Code, across all projects. Click it for the rest:

  • Idle threshold: 5m, 10m, 15m (the default), 30m, 1h, 2h or 4h.
  • Summary cards for all projects together: Today, This week, This month and Total.
  • Projects: the same four columns per project, most recently active first.

Click a project for its First and Last activity, Sessions, Prompts, Work blocks, a Last 14 days chart and a Reveal in Finder button.

The app rescans every minute, weeks start on Monday, and Launch at login keeps it running. The screenshots show demo projects.

The CLI prints the same numbers:

claude-time                          # all projects: today, week, month, total
claude-time api                      # projects matching "api", day by day
claude-time --idle 30 --days 30 api  # another threshold, a longer breakdown
claude-time --json                   # machine-readable

The overview ends with an ALL (merged) row, and --no-cache forces a full rescan.

How it measures time

Claude Code saves each session as a JSONL file under ~/.claude/projects, one folder per working directory. The records carry timestamps: your prompts, Claude's replies, each tool call and its result, sub-agents included.

Claude Time merges the timestamps of a project's sessions into one sorted timeline. A gap of up to the idle threshold between consecutive timestamps counts as work; a longer one doesn't count at all. With the default 15 minutes:

09:00  prompt        ┐
09:01  tool call     │  the test suite starts
09:09  tool result   │  8m of waiting: counted
09:14  reply         │
09:25  prompt        │  11m of reading: counted
09:31  reply         ┘  block 1: 31m
       1h 14m lunch     not counted
10:45  prompt        ┐
10:57  reply         ┘  block 2: 12m
       28m away         not counted
11:25  prompt        ┐
11:31  reply         ┘  block 3: 6m

Counted: 49m    First to last record: 2h 31m

Each stretch is a work block. Since sessions are merged first, an hour in two sessions at once counts once:

Session A   09:00 ████████████████ 09:40
Session B   09:10     ██████████████████ 09:55
Merged      09:00 ██████████████████████ 09:55   55m, not 40m + 45m

The summary cards and the ALL (merged) row merge all projects the same way, so they can be smaller than the sum of the project rows. Blocks are clipped to each window: one that runs past midnight is split between two days.

Why not session length?

Session length counts the lunch break and, for a session left open, the night. Counting prompts misses the minutes Claude spends running tools. Timestamps capture both: they keep coming while Claude works and stop when you walk away.

It has limits, too:

  • It measures time with Claude Code, not total work time. Your editor, browser and meetings don't show up.
  • A pause longer than the threshold counts as zero, not 15 minutes. If you often think for 20 minutes between prompts, raise the threshold.
  • Reading the final reply before you get up isn't counted: a block ends at its last timestamp.

Fast enough to run every minute

Claude Time doesn't parse JSON; decoding every line to read one field would be the slow part. It maps each file into memory where possible, finds "timestamp":" with memmem and reads the date digits in place, without a date formatter. The same search reads the working directory after "cwd":" and counts "role":"user","content":", which matches user messages with plain-text content (your prompts, plus the task prompts handed to sub-agents); tool results have an array there, not a string.

Scanner.swift
/// Calls `body` with the byte offset immediately after each match of `pattern`.
static func forEachOccurrence(of pattern: [UInt8], in base: UnsafeRawPointer, count n: Int,
                              stopAfterFirst: Bool = false, _ body: (Int) -> Void) {
    let plen = pattern.count
    pattern.withUnsafeBytes { pbuf in
        var offset = 0
        while offset + plen <= n,
              let hit = memmem(base + offset, n - offset, pbuf.baseAddress, plen) {
            let idx = base.distance(to: UnsafeRawPointer(hit))
            body(idx + plen)
            if stopAfterFirst { return }
            offset = idx + plen
        }
    }
}

Results are cached per file in ~/Library/Caches/claude-time/cache.json and reused while size and modification time match, so a rescan only reads the files that changed, usually just your current session. The first scan takes about half a second and later ones about 10 ms. Even 100+ MB of transcripts take well under a second.

The trade-off: it relies on Claude Code's compact JSON and key order. If those change, so must the patterns.

What you can do with it

Client timesheets

Keep each client's repos under one folder. The filter matches any part of the path, and in JSON mode allProjects merges the matches without double counting. A month of daily hours as CSV, for a spreadsheet or an invoice:

claude-time --json --days 45 clients/acme \
  | jq -r '.allProjects.daily[]
           | select(.day | startswith("2026-09"))
           | [.day, (.seconds / 3600 * 100 | round / 100)]
           | @csv' > acme-2026-09.csv

It's Claude Code time only: a starting point, not the whole timesheet.

Day job versus side projects

Same idea for any split you keep on disk. This week's hours per area, overlaps removed:

claude-time --json ~/work/ | jq '.allProjects.weekSeconds / 3600'
claude-time --json ~/side/ | jq '.allProjects.weekSeconds / 3600'

A weekly review

On Friday, claude-time shows the week per project, and claude-time --days 7 api shows which days a project got.

Estimating features

Claude Code keeps a folder per working directory, so a git worktree per feature gets its own row, with its own hours, prompts and work blocks:

git worktree add -b checkout ../shop-checkout
cd ../shop-checkout && claude
claude-time checkout   # when the feature is done

Estimate first, compare after, and a few features in you have numbers instead of a feeling.

Spotting long days

The 14-day chart makes heavy days obvious, and Last shows how late a project's last session ran. Days over ten hours in the last month:

claude-time --json --days 30 | jq -r '.allProjects.daily[] | select(.seconds > 10 * 3600) | .day'

Team reporting

There's no server or team mode. Each person runs the CLI and shares what they choose:

claude-time --json | jq '[.projects[] | {name, hours: (.weekSeconds / 3600)}]'

Use it for planning, not for comparing people: it measures time, not output.

My own numbers

With the default 15-minute threshold, as of today:

MetricValue
Projects15
Sessions133
Prompts2,191
Total since June 20160.7 h
This month (September)119.8 h
This week (since Monday)26.9 h
Active days (last 30)25
Hours per active day (last 30)5.2 h
Longest day (last 30)12.2 h

Total starts on June 20 because that's as far back as my transcripts go. What stood out:

  • 17 hours of overlap. My project rows add up to 177.7 hours, but the merged total is 160.7. The difference is sessions in different projects running side by side; a simple sum would count more than two working days twice.
  • A normal day is five hours. 25 of the last 30 days had activity, at 5.2 hours on average.
  • The longest day in the last 30 was 12.2 hours. I'd rather see that in the menu bar on the day than in a report later.
  • Typing is the small part. 2,191 prompts in 160.7 hours is one every 4.4 minutes.

Privacy: nothing leaves your Mac

Claude Time only reads Claude Code's transcripts and its own cache and settings. It makes no network requests and needs no plugin, hooks or extra logging. The cache holds paths, sizes, timestamps and counts, not your prompts. And since the data is already on disk, the first launch shows your history.

One caveat: Claude Code deletes transcripts after 30 days by default, and Total can't reach further back than they do. To keep more, raise cleanupPeriodDays in ~/.claude/settings.json, knowing the full transcripts then stay on disk longer too:

{
  "cleanupPeriodDays": 365
}

Install

Get version 0.1.0 from the Releases page: a universal (Apple Silicon and Intel) app zip and a CLI zip. It needs macOS 14 or later.

The app isn't notarized, so macOS blocks the first launch. On macOS 14, right-click the app and choose Open. From macOS 15 on, that shortcut is gone: open it once, then click Open Anyway in System Settings → Privacy & Security. Or remove the quarantine flag:

xattr -dr com.apple.quarantine /Applications/ClaudeTime.app

For the CLI, put claude-time on your PATH and try claude-time --version. If macOS blocks it, the same xattr command works on the binary.

To build from source (Swift 5.9 or newer):

git clone https://github.com/zgrgrcn/claude-time.git
cd claude-time
./Scripts/make-app.sh --install

This builds release binaries, copies ClaudeTime.app to /Applications, links the CLI to ~/.local/bin/claude-time (put that folder on your PATH) and opens the app. The link points into the clone, so keep it.

What's next

Ideas, not promises: a CSV export so the jq step goes away, weekly charts next to the daily one, and Linux support for the CLI, since Claude Code runs there too. Issues and pull requests are welcome on GitHub.

Claude Time is an independent project, not affiliated with or endorsed by Anthropic. Claude and Claude Code are trademarks of Anthropic.

It started as a question I couldn't answer. Now the answer sits in my menu bar and updates every minute.