Time tracking
Personal Claude Code skills for freelance and product engineering work
npx -y skills add taewanu/skills --skill time-trackingAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Track per-session work time on freelance/personal projects with automatic timestamps, timezone, and category breakdowns. Supports multiple open sessions via switch / pause+resume / concurrent. Use whenever the user wants to start, end, pause, resume, switch, analyze, or invoice a work session, including phrases like "시작", "끝", "일시정지", "재개", "전환", "갈아타", "타임 트래킹 시작", "세션 종료", "이번 주 분석", "분석해줘", "<client> 청구서", "start tracking", "end session", "pause session", "resume", "switch to <project>", "analyze hours", "invoice for X", or simply invoking the skill via /time-tracking. Triggers on any work-session timing intent, even if the user doesn't say "track" or "skill" explicitly.
SKILL.md
22.1 KB, as published. Nobody here has run it
Time Tracking
Records work sessions to per-project project_time_tracking.md files. Designed for solo freelance/personal work where the goal is:
- Retrospective analysis: "How much time did I spend on debugging this month?"
- Client invoicing: generate billable totals using rates from
~/.claude/billing_rates.md - Zero friction: auto-capture timestamps, timezone, project; user only supplies what can't be inferred
Invocation
This skill is invoked via the /time-tracking slash command, or by natural-language phrases matching the description.
Sub-actions are determined by the user's words:
| Sub-action | User phrases (Korean / English) |
|---|---|
start | "시작", "시작할게", "start", "start tracking", "begin session" |
end | "끝", "종료", "마쳤어", "end", "stop", "wrap up" |
pause | "일시정지", "잠깐 멈춰", "pause" |
resume | "재개", "다시 시작", "resume" |
switch | "전환", "갈아타", "switch", "switch to" |
discard | "폐기", "버려", "지워", "discard", "drop" |
status | "상태", "지금 세션", "status", "what's open" |
analyze | "분석", "이번 주", "this week", "analyze", "breakdown" |
invoice | "<client> 청구", "invoice for <client>", "bill <client>" |
If /time-tracking is invoked with no further input, default to status. After showing status, list the available sub-actions so the user can pick.
Asking questions
For any step that picks among a fixed set of choices, use the AskUserQuestion tool instead of printing the options as text: so the user selects or presses a number rather than typing the answer. This covers the Session-conflict flow and the Stale-session flow (in references/start-subflows.md), the "which session?" pickers in end / pause / resume / discard, the end category-proposal confirm (그대로 / 수정), and "Mark all as invoiced?".
The lettered blocks in this spec are the option content, not a literal prompt: map each letter to one AskUserQuestion option (label = the short choice, description = what it does), keep the order, and put any context the user needs to decide (elapsed time, conflicting sessions) in the question text.
Two carve-outs stay as typed prompts:
- Free-text input: the
endcombined prompt (category %, shipped / slipped lines) and anyHH:MM/ date entry can't be enumerated. - Destructive
(y/N)confirmations:discardand the stale (c) "폐기" branch keep the typed(y/N)gate; the friction is deliberate for an irreversible delete.
If AskUserQuestion isn't available in the current context, fall back to printing the lettered block and waiting for a typed reply.
Sub-action: start
-
Capture start time: run
date -u +"%Y-%m-%dT%H:%M:%SZ"for UTC ISO, thendate +"%H:%M %Z"for local display. Read system timezone viadate +%Zand full TZ name viareadlink /etc/localtime | sed 's|.*/zoneinfo/||'(macOS/Linux); read it from the system rather than asking the user. -
Auto-detect project:
- Run
pwd. - Extract project name from last directory segment, Title Case it (
<project-slug>→ "<Project Name>"). - If user passed a project name explicitly, use that instead.
- Run
-
Resolve tracking file path:
- Default:
~/.claude/projects/<encoded-cwd>/memory/project_time_tracking.md - Encoding:
pwdwith/replaced by-, leading-kept. E.g./Users/me/projects/example-app→-Users-me-projects-example-app. - If
memory/dir doesn't exist, create it. - If tracking file doesn't exist, create it with the header from
templates/tracking_file_header.md. - If the user names a different path (legacy or preference), use it and store the override in the session state.
- Default:
-
Check for existing sessions: read
~/.claude/time-tracking-state.json.- If
sessionslist is empty, proceed to step 5. - If a session for the same project is already
active, tell user이미 진행 중: <project> (Xh Ym)and stop: one active session per project. - If a session for the same project is
paused, offer:<project>이 일시정지 상태야 (누적 Xh Ym, 마지막 멈춤 HH:MM). (a) 재개 (resume) (b) 새 세션으로 따로 시작 (드물게 — 기존 paused는 그대로 둠) (c) 취소 - If sessions exist on different projects, first check for staleness: compute each session's
latest_activityfrom its segments (active → latestsegments[-1].start_iso; paused → latestsegments[-1].end_iso). A session is stale ifnow - latest_activity > 12h. If at least one stale session is found, follow the Stale-session flow inreferences/start-subflows.md(cleanup, not live switching). Otherwise follow the Session-conflict flow in the same file (both sessions alive). State file mtime is not used: staleness is per-session, so opening a new session doesn't hide another's staleness.
- If
-
Read previous slipped (optional context): from same-project tracking file, find the most recent entry, extract
slipped:line if present. -
Write state file: append a new session object to the
sessionslist (see schema in §"State file"); the list can hold multiple. -
Confirm to user (concise):
<HH:MM> <TZ>, <project> 시작. 이전 slipped: <one line>. 이어가?If no previous slipped, drop that clause. If other sessions are still active or paused, add one line:
진행 중인 다른 세션: <other-project> (<HH:MM>–, Xh Ym).
Sub-action: end [project] [--at <time>]
-
Read state: load
sessionsfrom state file. Pick the target session:- If user passed a project name (e.g. "end <project>" / "<project> 끝"), match by project name (case-insensitive).
- Else if exactly one
activesession exists, use it. - Else if multiple active sessions exist, list them and ask:
어느 세션 끝낼까? (a) <project-A> (b) <project-B> .... - Else if no sessions at all, tell user
진행 중인 세션 없어and stop.
-
Capture end time: same as start. If the user passed
--at <time>(or natural-language equivalents like "어제 17:00" / "at 5pm"), parse to ISO using the session's TZ. Must be ≥ the last segment's start_iso: otherwise error and ask again. Show the parsed result back to the user before writing:종료 시각: 2026-05-19 17:00 ICT, 맞아? -
Compute duration: sum of
(segment.end - segment.start)for each segment in the session, with the final open segment's end set to the capture time. Hours, 2 decimal places. If the session was ever paused (more than one segment), also compute the paused gap =(end of session window) - (sum of segments), formatted asXh Ymfor the entry note (not decimal hours: the gap is auxiliary info, not billable time). -
Date boundary check: if the session's first start date ≠ end date, split into multiple entries (one per date, with midnight as the split point). Run the rest of this flow for each split. For multi-segment sessions, attribute each segment to its own date first, then merge same-date segments.
-
Propose the category split; ask the rest (one combined prompt). Estimate the category % yourself from the session's actual work (the conversation, files touched, commands run): keys from the 8 allowed, sum ~100, 5–10 unit granularity. Present it as a decided proposal and ask only whether to adjust it. The other fields stay open questions.
16:45 ICT, 2.25h. 카테고리 (제안): infra 30, decisions 25, meta 20, debugging 15, other 10 이대로 갈까? 바꿀 거 있으면 말해줘. Shipped (한 줄)? Slipped (있으면)? Retro memory 파일명 (있으면)? Billable client (없으면 'none')?Write the entry only once the categories are explicitly confirmed. Gate the proposal through AskUserQuestion (그대로 / 수정): on 그대로, keep it; on 수정, take the new split. If the reply is ambiguous, ask once more (
카테고리 이대로 갈까?) before writing, rather than assuming acceptance. -
Validate categories: keys must be from the 8 allowed (
planning,design,decisions,implementation,debugging,infra,meta,other). Sum should be ~100 (allow 95–105). If invalid, ask again. -
Build entry using the format in
references/entry-format.md. -
Append to tracking file:
- Find the
### YYYY-MM-DDheader for the entry's date. - If header doesn't exist, create it in chronological order (newest at top under
## Entries). - Append the entry under that date header.
- Find the
-
Update state file: remove this session from the
sessionslist (other sessions, if any, untouched). -
Show user the written entry for confirmation.
Switch shortcut mode
Triggered by the Session-conflict flow picking (a), or by the user invoking switch <new-project> directly (see §"Sub-action: switch").
Behaves like end but with a minimal prompt: only shipped is asked. Other fields are filled as placeholders for later manual edit:
cat:TBD(no key/pct pairs)slipped: omittedretro: omittedbillable:TBD
Entry is still written and the session is removed from state, but the entry includes a trailing sub-bullet: - needs-edit: cat, billable so the user can grep for incomplete entries later. The switch confirmation tells the user which entry to edit, e.g. <HH:MM> <TZ>, <project> 마감 (draft: categories/billable 나중에 편집).
Sub-action: status
- Read state file.
- If
sessionslist is empty: respond진행 중인 세션 없어. 어떤 작업을 할지 알려줘: - start — 세션 시작 - resume — 일시정지된 세션 재개 - analyze — 시간 분석 - invoice — 청구서 생성 - Else, show each session grouped by status. Within each group, sort by the session's
start_isoascending so sessions appear in the order they were opened.
Omit the진행 중 (N): - <project>: <HH:MM> <TZ> (Xh Ym 경과) [active] 기록 파일: <path> - <project>: <HH:MM> <TZ> (Xh Ym 경과) [active] 기록 파일: <path> 일시정지 (M): - <project>: <HH:MM>–<HH:MM> <TZ> (Xh Ym 누적, 마지막 멈춤 HH:MM) 기록 파일: <path>일시정지block if no paused sessions; omit the진행 중block if all are paused. If any session is stale (criteria fromstartstep 4), flag it with⚠️ Xh 전부터 안 닫힘after the elapsed time so the user sees it without runningstart.
Sub-action: pause [project]
- Read state. Pick target session (same rules as
endstep 1, but onlyactivesessions are eligible: if all are paused, say so and stop). - Capture pause time (same
datecalls asstart). - Close the open segment: set its
end_isoto the pause time. - Set the session's
statustopaused. - Confirm:
<HH:MM> <TZ>, <project> 일시정지 (누적 Xh Ym).
No questions are asked: this is a pure state change. Categories, shipped, etc. are deferred to end.
Sub-action: resume [project]
- Read state. Find target session:
- If user passed a project name, match against
pausedsessions. - Else if exactly one
pausedsession exists, use it. - Else if multiple, list and ask.
- Else if none, tell user
일시정지된 세션 없어and stop.
- If user passed a project name, match against
- Capture resume time.
- Append a new open segment:
{start_iso: <now>, end_iso: null}. - Set the session's
statustoactive. - Confirm:
<HH:MM> <TZ>, <project> 재개 (누적 Xh Ym).
Note: resume does not pause any other currently-active session. If the user wants a strict swap, they should pause <other> first, then resume <this>.
Sub-action: switch <new-project>
Shortcut for "close the current session quickly and start a new one." Equivalent to picking (a) in the Session-conflict flow.
- If no session is active, behaves like
start <new-project>. - If exactly one active session exists, run §"Switch shortcut mode" of
endon it, thenstartthe new project. - If multiple active sessions exist, ask which one to switch from (or whether to close all). Then proceed.
If the user just says switch with no project name, prompt for one. switch takes an explicit project name (unlike start, it doesn't auto-detect from cwd).
Sub-action: discard [project]
Drops a session from state without writing any tracking-file entry. For when the user realizes a session was opened by accident or was forgotten and the time is lost beyond recovering.
- Read state. Pick target (same rules as
end: explicit project name, else single session, else list and ask). - Confirm before destroying (this is unrecoverable):
<project> 폐기? 시작 <HH:MM> <TZ>, 누적 Xh Ym. (y/N) - On
y, remove the session from thesessionslist. Confirm:<project> 폐기됨. - On
Nor anything else, abort.
Unlike end, discard writes nothing to the tracking file: the session's time is forgotten on purpose.
Sub-action: analyze [period]
-
Resolve period to start/end dates. Default
this-week. -
Glob tracking files:
~/.claude/projects/*/memory/project_time_tracking.md. -
Parse entries from all files, both formats, per
references/entry-format.md. -
Filter by date range.
-
Aggregate:
- Total hours
- By project (hours, %)
- By category (hours, %, weighted by entry duration × category %)
- By tool (claude-code, claude-web, etc.)
- By location
-
Output (plain text, no chart):
Analysis — this-week (2026-05-13..2026-05-19) Total: 18.5h across 7 sessions By project: - <Project A>: 14.2h (76.8%) - <Project B>: 4.3h (23.2%) By category: - implementation: 7.4h (40%) - decisions: 3.7h (20%) - debugging: 2.8h (15%) ... By tool: - Claude Code: 16.1h - Claude.ai web: 2.4h By location: - Bangkok: 14.2h - Korea: 4.3h
Sub-action: invoice <client> [period]
-
Read
~/.claude/billing_rates.md. If missing, tell user to create it (show template). -
Find rate for
<client>in the Active section. If not found, list available clients. -
Resolve period, default
this-month. -
Glob and parse entries (same as analyze).
-
Filter: entries with
billable:<client>tag AND noinvoiced:tag. -
Compute: total hours × rate.
-
Output:
Invoice draft — my-client-app — 2026-05-01..2026-05-31 Rate: 80 USD/h Total: 23.45h × 80 USD/h = 1,876.00 USD Entries (5): - 2026-05-03 (Sun) 10:00–14:30 ICT (4.50h) — MyClientApp | landing page - 2026-05-05 (Tue) 09:00–13:00 ICT (4.00h) — MyClientApp | checkout flow ... Category breakdown: - implementation: 15.3h (65%) - design: 4.7h (20%) - debugging: 2.3h (10%) - meta: 1.2h (5%) Mark all as invoiced? (y/N) -
If user confirms, add
invoiced:<YYYY-MM-DD-N>tag to each entry'stags:line (where N is an incrementing counter). The id is auto-generated, format<today>-<counter>.
Period syntax
today, this-week, last-week, this-month, last-month, 2026-05, 2026-05-01..2026-05-15.
Defaults: analyze → this-week, invoice → this-month.
Categories (fixed, 8)
planning, design, decisions, implementation, debugging, infra, meta, other. What each covers and a decision tree for ambiguous cases: references/category_guide.md.
State file
Path: ~/.claude/time-tracking-state.json
{
"sessions": [
{
"id": "<project-slug>-<YYYYMMDD>-<HHMM>",
"project": "<display name>",
"start_iso": "2026-05-19T14:30:00+07:00",
"tz_display": "ICT",
"tz_full": "Asia/Bangkok",
"location": "Bangkok",
"tool": "Claude Code",
"tracking_file": "/Users/me/.claude/projects/.../memory/project_time_tracking.md",
"previous_slipped": "<one line, from last entry in tracking file>",
"status": "active",
"segments": [
{"start_iso": "2026-05-19T14:30:00+07:00", "end_iso": null}
]
}
]
}
The sessions list can hold multiple entries. Each session is independent: it has its own segments, status (active or paused), and tracking file.
Staleness derivation: there is no stored last_touched field. Compute it on demand from segments: active session: latest segments[-1].start_iso; paused session: latest segments[-1].end_iso. Every meaningful mutation already writes a segment (start opens segment 0, pause closes the open one, resume appends a new open one), so the segment timestamps capture the same information without duplication.
Segments: a session is a list of {start_iso, end_iso} segments. The currently-open segment has end_iso: null. Pausing closes the open segment (set end_iso to now) and flips status to paused. Resuming appends a new {start_iso: now, end_iso: null} and flips status back to active. Duration = sum of (end - start) across all segments, with null end treated as "now" for live computation.
id: stable identifier for the session, used to disambiguate when multiple sessions share a project name (rare but possible after a switch-then-restart, or after the paused-same-project (b) "새 세션으로 따로 시작" branch). Format: <project-slug>-<YYYYMMDD>-<HHMM> from the first segment's start. When end, pause, resume, or discard is invoked with only a project name and two sessions share it, list both by id (annotated with start time) and ask which.
Location inference: if tz_full is Asia/Bangkok → "Bangkok", Asia/Seoul → "Korea", America/Vancouver → "Vancouver", else use the TZ name. User can override mid-session by saying "I'm in <place>".
Migration from v1 single-session shape: if the state file has a top-level current_session object (old format), read it once, wrap it as {sessions: [<old-object-with-status:active-and-single-segment>]}, and rewrite. Don't fail on the legacy shape.
Billable rates file
Path: ~/.claude/billing_rates.md (user-created; Skill reads only).
Template: templates/billing_rates.example.md.
Tone and behavior
- Category % is proposed, then confirmed. Estimate the split from the session's actual work, present it as a proposal, and proceed on the user's OK; apply their adjustment only if they want one.
- Keep confirmations dry.
<HH:MM> <TZ>, <project> 시작., not "🚀 시작합니다!" - Show inferred values. When auto-detecting project from cwd or splitting across a date boundary, show the user what was inferred and let them override.
- Match the user's language for confirmations (Korean or English).
- Only
/time-trackingis a slash command. The sub-actions (start,end,pause,resume,switch,discard,status,analyze,invoice) are natural-language words. - One-line confirmations for state changes.
pause,resume,discard,switchwrite nothing to the tracking file (exceptswitch's draft entry). Confirmations stay to one line, matching the dryness ofstart/end: e.g.<HH:MM> <TZ>, <project> 일시정지 (누적 Xh Ym). - Confirm before destructive action.
discardand the stale-session (c) "폐기" branch destroy time data permanently: always show a(y/N)prompt with the time about to be lost, and act only on an explicity. - Use
<project>/<client>/<HH:MM>placeholders in prompts; the names fill at runtime from state.
Edge cases
- System clock changed mid-session: trust the captured ISO timestamps. Don't recompute from "now".
- Session crosses DST: compute duration from ISO timestamps (which carry offset), not wall-clock HH:MM.
- Tracking file manually edited: re-parse on every read. No caching.
billing_rates.mdhas a client in "Archived": still resolvable for historical invoices but warn user.- Category sum 95–105: accept and normalize internally. <95 or >105: ask user to fix.
- Very short session (<5 min): still record. User decides if it's meaningful.
- Pause spans midnight: when an open segment crosses midnight, the date-boundary split in
endstep 4 still applies to each individual segment. A session with segments[09:00–13:00 day-N, 22:00–01:30 day-N+1]produces three entries: full day-N (12:00 chunk + 22:00–24:00), and 00:00–01:30 on day-N+1. The paused gap (13:00–22:00) is just ignored: it belongs to no day. - Forgotten / abandoned session: handled by the Stale-session flow of
start, and visible via the staleness flag instatus. The user can also invokediscard <project>orend <project> --at <time>directly without going throughstart. - Concurrent sessions overlapping in wall time: deliberately allowed: the spec records the literal segments and
analyzesums them straight. If you billed 2h to client A and 2h to client B in the same 13:00–15:00 window, total comes out as 4h. See §"What this Skill does NOT do" for the analyze-side caveat.
What this Skill does NOT do
- Idle detection / auto-pause (pause is explicit only)
- Charts / visualizations
- Sync with external trackers (Toggl, Harvest)
- Pomodoro / notifications
- Migrating legacy entries to slim format (kept as-is, parsed compatibly)
- Auto-recovery of forgotten sessions: stale sessions (>12h) are flagged in
statusand on nextstart, but never silently closed; user picks the resolution - Overlap deduplication in
analyze: two concurrent sessions in the same wall-clock window are both summed into the total. Future: surface aNote: Xh of overlap across N concurrent sessionsline under the total so the user notices.