Skip to content

feat: track Claude Code Desktop sessions - #14

Merged
iamnotstatic merged 2 commits into
iamnotstatic:mainfrom
joshuanwankwo:feat/track-claude-code-desktop
Aug 3, 2026
Merged

feat: track Claude Code Desktop sessions#14
iamnotstatic merged 2 commits into
iamnotstatic:mainfrom
joshuanwankwo:feat/track-claude-code-desktop

Conversation

@joshuanwankwo

Copy link
Copy Markdown
Contributor

Track Claude Code Desktop sessions

Problem

vibe init wraps the claude terminal command via shell hooks, so a session is only tracked when you launch claude from a shell. The Claude Code Desktop app never invokes that command, so vibe __wrap never fires and Desktop work is invisible to vibetime. For people who work mostly in Desktop, none of their time is tracked.

Approach

Track Desktop the way Claude Code exposes itself to tooling: session hooks. vibe hooks install registers hooks in ~/.claude/settings.json (the same settings the Desktop app reads) that call a hidden vibe __hook <event> on each session lifecycle event:

Claude hook vibe action
SessionStart open a session (record start, HEAD sha, project, branch)
UserPromptSubmit / PostToolUse / Stop mark activity, accumulate active time, refresh git stats
SessionEnd finalize duration + git diff, score momentum, submit to the leaderboard

Desktop sessions land in the same ~/.vibe/sessions.json with tool: "claude", so they show up in vibe status, vibe log, vibe share, and the leaderboard exactly like terminal sessions. No server changes — the payload is unchanged and the Claude session_id is already a UUID the API accepts.

Duration + idle

Active time is accumulated across hook events and each idle gap is capped at the 30-minute inactivity timeout — the same idle-exclusion rule the terminal poller uses, so Desktop and terminal durations are comparable. A single git baseline sha is persisted on the session (startSha) because start and end now happen in separate processes.

Notes / decisions

  • No double-counting. Claude Code fires these hooks inside a shell-wrapped claude too. The handler short-circuits when VIBE_SESSION=1, so if you run both vibe init and vibe hooks install, terminal sessions are counted once.
  • Absolute paths. The Desktop app launched from the Dock does not inherit the shell PATH, so a bare vibe would not resolve. vibe hooks install pins absolute paths to Node and cli.js.
  • Privacy preserved. Hooks read only session_id and cwd from the payload — never the transcript, prompts, or model output — and derive the same git metadata as the wrapper. Updated the Privacy section to say so.
  • Safe settings merge. Install merges into existing ~/.claude/settings.json without touching other keys or the user's own hooks, is idempotent, and refuses to write if the file is not valid JSON. vibe hooks uninstall removes only vibe's hook entries.
  • Silent + safe hooks. vibe __hook writes nothing to stdout (SessionStart stdout is fed to the model), swallows all errors, and always exits 0, so it can never disrupt a session.

Testing

Verified end-to-end against a throwaway $HOME and a real git repo: install/merge/idempotency/uninstall, session-start → activity → session-end lifecycle, git-diff scoring, idle-gap capping, the VIBE_SESSION dedup, non-UUID/empty-stdin/unknown-id/non-git edge cases. tsc clean.

Usage

vibe hooks install     # open a new Desktop session; it is now tracked
vibe hooks uninstall   # stop tracking Desktop

vibe init only wraps the claude terminal command, so Desktop app sessions were never tracked. Add vibe hooks install/uninstall that register SessionStart/UserPromptSubmit/PostToolUse/Stop/SessionEnd hooks in ~/.claude/settings.json to record Desktop sessions the same way: idle-excluded duration, git-based scoring, wrapper dedup via VIBE_SESSION, absolute hook paths for Dock launches. No server changes.
@iamnotstatic

Copy link
Copy Markdown
Owner

Thanks for this. The approach is exactly right: hooks are how Desktop exposes itself to tooling, the privacy model is preserved (session_id and cwd only), no new dependencies, and no server changes needed. I verified the VIBE_SESSION=1 dedup guard works, the settings merge is safe and idempotent on a normal install, and tsc is clean on the branch.

One blocker. reapOrphanedSessions finalizes any exitCode: -1 session idle over 30 minutes. The terminal wrapper survives that because its poller rewrites the row every 30s. Hook sessions only write on hook events, and the reaper runs on every vibe status / log / share and inside onSessionStart. So: Desktop session idle 40 min, vibe status runs, session gets marked interrupted. User comes back and commits, but activity and session-end both bail on the exitCode !== -1 check. Session ends commits: 0, momentum: interrupted even though work shipped. Reproduced end to end. A lunch break triggers it.

Fix I'd take: in onActivity, if the id matches a session the reaper closed, reopen it. exitCode back to -1, clear submittedAt so the final state resubmits (server upserts on id, so that's safe). activeSecondsSince already caps the gap.

Two nits:

  • isVibeHook matches command.includes('vibe') case-sensitively. On a dev clone without lowercase "vibe" in the path, reinstall duplicates the hooks and uninstall misses them. Matching __hook alone is enough.

  • The process.execPath pin breaks under nvm once the old node version is removed. A README note to re-run vibe hooks install after a node upgrade covers it for now.

onActivity and onSessionEnd now reopen a hook session that reapOrphanedSessions finalized as interrupted after 30 min idle, so work shipped after a lunch break is finalized instead of lost; the reopened state clears submittedAt and resubmits since the server upserts on id. Also match our hooks by the __hook token alone so reinstall and uninstall stay reliable on dev clones, plus a README note to re-run vibe hooks install after a Node upgrade.
@joshuanwankwo

Copy link
Copy Markdown
Contributor Author

Great catch on the reaper race.

Fixed:
onActivity and onSessionEnd now reopen a session the reaper closed as interrupted: flip exitCode back to -1, clear submittedAt so the corrected state resubmits (the id upsert makes that safe), and restart the active-time clock from now so the idle gap isn't re-counted. Git stats recompute from startSha, so the commits come back. Verified end-to-end: reaped → interrupted, commit, then the next hook event finalizes as shipped with the work intact and duration bounded at the reaper's cap.
isVibeHook now matches on __hook alone.
Added the README note about re-running vibe hooks install after a Node version change.

@iamnotstatic iamnotstatic left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified on the branch. Ran the original repro: 40 min idle, reap via vibe status, commit, then activity and session-end. The session reopens, the commit comes back, duration stays at the reaper's cap, and the corrected final state resubmits. Also confirmed install is idempotent and uninstall is clean on a clone path with no lowercase "vibe" in it, which was the failing case. tsc clean.

Nice work on the recovery logic, the restart-the-clock approach keeps durations honest without re-counting the gap. Merging.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants