Automatic time tracking plugin for Claude Code. Logs session activity as JSONL for timesheet reconstruction.
Use it to:
- Build client timesheets — see how long you spent on each project
- Track your own time across projects and tickets without manual timers
- Generate weekly/monthly reports for invoicing or personal review
- Audit which tickets consumed the most Claude usage
- Session start/end with timestamps
- Every prompt you submit (first 500 characters)
- Project name from file paths, git repository root, or working directory
- Ticket/issue number from git branch name or prompt text
- Model used for the session
All data stays local. Nothing is sent to external services.
From marketplace (recommended)
/plugin marketplace add RemoteCTO/claude-plugins-marketplace
/plugin install timelog
git clone https://github.com/RemoteCTO/claude-code-timelog.git
claude --plugin-dir ./claude-code-timelogThat's it. The plugin starts logging immediately with sensible defaults:
- Project = git repository name
- Tickets = Jira/Linear IDs from branch
names (e.g.
BAN-123,PROJ-456) - Logs written to
~/.claude/timelog/
No configuration needed for basic use.
After your first prompt, check that a log file was created:
ls ~/.claude/timelog/You should see a file named with today's
date (e.g. 2026-02-13.jsonl). If not,
check the plugin is installed with /plugin.
To import time from previous sessions:
/timelog:backfill
If you launch Claude Code from a parent
directory (e.g. ~/projects/) rather than
individual project directories, all time
gets lumped under one project name. Fix
this by asking Claude:
"Help me set up my timelog projectPattern — here's my directory structure"
Claude will examine your directories and
generate the right regex for your
~/.claude/timelog/config.json. See
Project detection
for details.
The claudelog command lets you run reports
and backfills from any terminal — no active
Claude Code session needed.
claudelog # default report (--week)
claudelog report --month --timesheet # explicit flags
claudelog backfill # import history
claudelog --help # show usageRunning claudelog with no arguments runs
a default report. Configure the default via
defaultReport in config.json:
{
"defaultReport": ["--month", "--timesheet"]
}Falls back to ["--week"] if not set.
The plugin installs to a versioned cache directory. Create a wrapper script that automatically finds the latest version:
cat > ~/.claude/bin/claudelog << 'WRAPPER'
#!/usr/bin/env bash
set -euo pipefail
PLUGIN_DIR="$HOME/.claude/plugins/cache"
PLUGIN_DIR+="/remotecto-plugins/timelog"
LATEST=$(
ls -1 "$PLUGIN_DIR" 2>/dev/null \
| sort -V | tail -1
)
if [ -z "$LATEST" ]; then
echo "timelog not installed" >&2
exit 1
fi
exec node \
"$PLUGIN_DIR/$LATEST/bin/claudelog" "$@"
WRAPPER
chmod +x ~/.claude/bin/claudelog~/.claude/bin is on PATH if you use Claude
Code (added by the installer). The wrapper
survives plugin version bumps — no need to
update it after upgrading.
The plugin needs to know which project you're working on. It tries three methods in order:
- File path regex (
projectPattern) — extracts the project name from file paths Claude touches during the session. - Git root (
projectSource: "git-root") — uses the git repository directory name. - Working directory (
projectSource: "cwd") — uses the directory name you launched from.
If you launch Claude Code from a project directory (the most common case), methods 2 and 3 work automatically. No config needed.
If you launch from a parent directory
(e.g. ~/projects/), everything gets lumped
under one name. Set projectPattern to fix
this — see Project detection.
Tickets are detected automatically from:
- Git branch name —
feature/BAN-123extractsBAN-123 - Prompt text — "Fix BAN-123 login bug"
extracts
BAN-123
The default pattern matches Jira, Linear,
Shortcut, and similar formats:
PROJECT-123. To add custom patterns
(GitHub issues, internal IDs), see
Ticket detection.
| Tip | Why |
|---|---|
| Launch from the project directory | Automatic project detection |
| Use ticket IDs in branch names | Automatic ticket attribution |
| One task per session when possible | Cleaner time attribution |
Name branches TICKET-description |
Branch-based detection is most reliable |
If you don't use tickets or branches, the plugin still tracks time per project and per day — just without the ticket breakdown.
Create config.json in your timelog
directory to customise behaviour. All
settings are optional.
{
"ticketPatterns": [
"([A-Z][A-Z0-9]+-\\d+)"
],
"projectSource": "git-root",
"projectPattern": null,
"breakThreshold": 1800,
"defaultReport": ["--week"]
}See config.example.json for a template.
# In your shell profile (.bashrc, .zshrc)
export CLAUDE_TIMELOG_DIR="$HOME/timelogs"Default: ~/.claude/timelog/
When the default git-root detection isn't
enough (e.g. you launch from a parent
directory), use projectPattern:
{
"projectPattern":
"/home/me/projects/(?:active/)?([^/]+)"
}The regex is applied to absolute file paths from tool calls (Read, Edit, Write, etc.). The first capture group becomes the project name.
How it works:
- Live hook: reads the tail of the current session transcript to find recent file paths from tool calls.
- Backfill: scans all tool calls in each transcript.
- Fallback: if no file paths match, uses
projectSourcedetection (git-root or cwd).
Building your pattern:
Start from your directory structure and skip any "grouping" directories that aren't project names:
~/projects/
my-app/ ← project
client-work/
acme/ ← project
widgets/ ← project
_archived/
old-thing/ ← project
Pattern: projects/(?:client-work/|_archived/)?([^/]+)
This captures my-app, acme, widgets,
and old-thing — skipping the intermediate
grouping directories.
More examples:
| Structure | Pattern |
|---|---|
~/projects/<name>/ |
projects/([^/]+) |
~/projects/active/<name>/ |
projects/(?:active/)?([^/]+) |
~/code/<org>/<name>/ |
code/[^/]+/([^/]+) |
~/work/<client>/<project>/ |
work/[^/]+/([^/]+) |
Anchoring tip: use enough path prefix
to avoid matching paths outside your
projects (e.g. ~/.claude/projects/ also
contains projects/ in the path). A full
home directory prefix is safest:
{
"projectPattern":
"/Users/me/projects/(?:active/)?([^/]+)"
}{
"ticketPatterns": [
"([A-Z][A-Z0-9]+-\\d+)",
"#(\\d+)"
]
}Array of regex strings tried in order. Use a capture group for the ticket ID; if none, the full match is used.
Detection sources (priority order):
- Git branch name
- Prompt text (first match)
Common patterns:
| Format | Pattern |
|---|---|
Jira/Linear (PROJ-123) |
([A-Z][A-Z0-9]+-\\d+) |
GitHub issues (#42) |
#(\\d+) |
Custom (TICKET-0042) |
(TICKET-\\d{4}) |
| None (disable) | [] (empty array) |
Time is calculated from gaps between consecutive events within a session:
- Gap under the threshold → active time
- Gap over the threshold → break (excluded)
"Active time" includes Claude's processing time and your review time between prompts — not just typing time. For billing this is typically the right metric: the client pays for the session of work, not keystrokes.
{
"breakThreshold": 1800
}| Value | Meaning |
|---|---|
600 |
Strict — 10 min break detection |
1800 |
Default — 30 min threshold |
3600 |
Lenient — 1 hour threshold |
/timelog:report
/timelog:report --month
/timelog:report --from 2026-02-01 --to 2026-02-14
Shows a daily breakdown with projects and ticket sub-items where available:
Date Project / Ticket Active Prompts
────────── ────────────────────── ──────── ───────
Mon 10 Feb infrastructure 12h 21m 159
BAN-139 9h 38m 133
MB-1 2h 42m 26
acme-api 4h 23m 39
PROJ-123 3h 51m 36
rails 49m 28
Projects without detected tickets show the project row only (no sub-items).
/timelog:report --timesheet
/timelog:report --timesheet --month
Aggregated across days. Best for monthly summaries and invoicing:
Project / Ticket Active Sess Prompts
────────────────────────── ──────── ──── ───────
infrastructure 46h 29m 11 686
BAN-139 22h 13m 3 341
BAN-142 7h 00m 1 90
NET-001 6h 47m 1 114
incubating 26h 12m 14 375
BAN-136 7h 23m 2 122
/timelog:report --by-project
/timelog:report --by-ticket
/timelog:report --by-model
/timelog:report --by-day
/timelog:report --project infrastructure
/timelog:report --ticket BAN-139
| Flag | Effect |
|---|---|
--week |
This week (default period) |
--month |
This calendar month |
--from DATE |
Start date (YYYY-MM-DD) |
--to DATE |
End date (YYYY-MM-DD) |
--timesheet |
Project x ticket summary |
--by-project |
Group by project |
--by-ticket |
Group by ticket |
--by-model |
Group by model |
--by-day |
Group by day |
--project NAME |
Filter to project |
--ticket ID |
Filter to ticket |
--json |
Structured JSON output |
CLAUDE_TIMELOG_DIR=~/timelogs \
node scripts/report.mjs --timesheetImport from existing Claude Code session transcripts:
/timelog:backfill
Or run the script directly:
node scripts/backfill.mjsThe backfill:
- Scans
~/.claude/projects/for all session JSONL transcripts - Extracts prompts, timestamps, and tickets
- Infers projects from file paths when
projectPatternis configured - Tags all entries with
source: "backfill"
Backfill is idempotent. Running it again
replaces previous backfill data but
preserves any entries from the live hook
(which have no source tag). This means
you can re-run after changing your
projectPattern without losing live data.
One JSONL file per day (YYYY-MM-DD.jsonl).
Each line is a JSON object:
{
"ts": "2026-02-12T14:30:00.000Z",
"event": "UserPromptSubmit",
"session": "abc-123-def",
"project": "my-app",
"ticket": "BAN-456",
"prompt": "Fix the login form validation"
}| Event | Extra fields |
|---|---|
SessionStart |
model, source |
UserPromptSubmit |
prompt, source |
SessionEnd |
reason, source |
All events include ts, session,
project, ticket, and cwd.
source is "backfill" for imported data,
absent for live hook data.
Reports aggregate at the event level, not the session level. Each consecutive pair of events within a session creates a "time slice" attributed to the first event's project, ticket, and model. This means:
- Mid-session project switches are tracked accurately
- Multi-ticket sessions split time correctly between tickets
- Concurrent sessions are handled cleanly (separate session UUIDs)
If you used the wrapper script from Adding to PATH, updates are automatic — the wrapper finds the latest installed version each time it runs.
- Check the plugin is installed:
/plugin - The first log file appears after your first prompt in a session
- Check the log directory exists:
ls ~/.claude/timelog/
The project name comes from (in order):
projectPatternregex on file paths- Git repository root directory name
- Working directory name
Common causes:
- Launched from
~/projects/instead of~/projects/my-app/— all time goes underprojects - Editing files outside your project tree — the regex doesn't match
- No
projectPatternset and no git repo
Fix: set projectPattern in
~/.claude/timelog/config.json. Ask Claude
to help — it can examine your directory
structure and generate the right regex.
- Run
/timelog:backfillto import from existing transcripts - Check the date range:
--weekonly covers the current week (Monday onwards) - Use
--fromand--tofor custom ranges
The default breakThreshold is 30 minutes.
Time between prompts under this threshold
counts as active time (including Claude's
processing time and your review time).
If you take frequent short breaks, lower the threshold:
{
"breakThreshold": 600
}If you regularly spend more than 30 minutes reviewing Claude's output between prompts, raise the threshold:
{
"breakThreshold": 3600
}- Claude Code 1.0.33+
- Node.js (ships with Claude Code)
- Git (optional, for ticket detection)
Apache 2.0 — see LICENSE.