A floating pixel-cat desktop pet for Claude Code. Rocky sits on top of your screen as a single animated cat whose mood reflects what your Claude Code sessions are doing β click it to see every session and jump straight to the one that needs you.
Native Swift/AppKit. Single ~190 KB binary, no dependencies, near-zero CPU when idle. macOS only.
Rocky floats on top of whatever you're working in. Its mood tracks your Claude Code sessions β click it to reveal them all and jump to the one that needs you.
π΄ needs permission Β· π’ your turn Β· π΅ working Β· βͺ idle
A single frosted, always-on-top widget β no window to manage, no tab to hunt for. Collapsed it's just the pet; click it and the sessions fan out.
Rocky ships an optional screen saver too: step away and your Mac shows the cat, a live clock, and every session's status β with a soft glow when one needs you. Glance across the room and know if a session is blocked, without unlocking.
- One hero pet (a ginger cat named Rocky) sits on your screen and animates with your overall mood β priority order: needs-permission βΊ your-turn βΊ working βΊ idle. A small badge shows the session count, turning red/green when a session needs you.
- Moods (hand-animated pixel art):
- a walk cycle with a springy tail β and a tiny keyboard tapping away β while Claude is working
- a happy bounce when a session finishes ("your turn")
- a shake with a π padlock when a session needs permission
- curls up breathing with a
zwhen everything's idle, then stretches awake when work resumes - a sparkly all-clear celebration when the last busy session goes quiet
- Click the pet to reveal the session tabs β they fan out with a staggered animation, one row per running session with a colour-coded status dot (π΄ needs permission Β· π’ your turn Β· π΅ working Β· βͺ idle), its name, and status. Click a tab to jump straight to that session's terminal tab. Click the pet again to collapse.
- Each tab tells the story, not just the state:
- a transcript peek of what a session is actually doing or asking ("Finished the migration β want me to run the tests?") when it needs you,
- an elapsed timer ("needs permission Β· 4m"), with a stronger pulse and a re-nudge when a session's been blocked too long (interval tunable from the right-click menu β "Re-nudge Interval" β default 2 minutes, or off),
- and a tiny activity sparkline so busy-vs-idle is obvious at a glance.
- Alerts when a session finishes or needs permission: a colored ring ripples out from the pet, its row pulses with a matching glow (green = done, red = needs permission), and a sound plays β all in Rocky itself, no macOS toast/banner. Nothing fires for routine tool calls. Rocky never dings for state it just discovered on launch β only for things that changed while it was watching (see Preferences to tune or silence this).
- A tiny preferences layer, entirely in the right-click menu β no config file, no settings window (see Preferences).
- Self-checks itself: the right-click menu always shows whether hooks are wired and whether Claude's session registry is readable, so a silent "why did the cat disappear" never happens without an answer.
- macOS (Apple Silicon or Intel), macOS 12+.
- Xcode Command Line Tools for
swiftc(xcode-select --install). - Claude Code installed.
- Terminal: Warp, iTerm2, Terminal.app, kitty (with remote control), VS Code, Cursor, or tmux in any host for click-to-exact-tab focus (other terminals still show sessions; click activates the app, and the tab says so).
brew install ketansomvanshi/tap/rocky
rocky-setup # connect Rocky to Claude Code's hooks
brew services start rocky # launch now + at loginHomebrew compiles the single Swift file locally, so there's no Gatekeeper
"unidentified developer" prompt. rocky-setup merges Rocky's hooks into
~/.claude/settings.json (your existing hooks, including Claude Island, are
left untouched); rocky-teardown removes them again.
git clone https://github.com/KetanSomvanshi/rocky.git
cd rocky
./install.shThis compiles Rocky, installs it to ~/.claude/rocky/, sets it to launch at
login (a launchd agent), and wires the Claude Code hooks into
~/.claude/settings.json (merged β your existing hooks, including Claude
Island, are left untouched).
Either way, open a fresh Claude Code session (or run /hooks in an existing
one) so the hooks load.
To also install the screen saver, add --with-screensaver to the source
installer, or build it directly:
./install.sh --with-screensaver # from source, alongside the widget
# or, standalone:
./screensaver/build.sh --installThen pick Rocky in System Settings β Screen Saver. It's a universal (Apple Silicon + Intel) build and reads the same session data as the widget.
Rocky merges two sources every 0.3s:
1. Claude's live session registry ~/.claude/sessions/<pid>.json
β the authoritative list of every running session (name, cwd, status).
This is why ALL sessions appear β no hooks required.
2. Rocky's hook data ~/.claude/rocky/sessions/<id>.json
β rocky-hook.py, fired by Claude Code hook events (async)
β adds real-time detail: which tool is running, needs-permission,
your-turn, and the finish/permission alerts.
Hooks run with "async": true, so they add zero latency to Claude's turns, and
if Rocky isn't running they're harmless no-ops. A session shows up the moment
it's running (from the registry); hooks just make its status richer.
| Action | How |
|---|---|
| Move the window | drag the pet anywhere (position is remembered) |
| Show / hide session tabs | click the pet |
| Jump to a session | click its tab |
| Mute / unmute one session | right-click its tab |
| Change a preference | right-click the pet (see below) |
| Launch at login | right-click β Launch at Login |
| Quit | right-click β Quit Rocky |
The window is a non-activating panel: clicking it never steals keyboard focus from your terminal, and clicks register on the first try.
No config file, no settings window β every knob lives in the right-click
menu and persists the same way the window position already does
(UserDefaults, nothing written to disk you'd need to hand-edit).
| Preference | Options | Notes |
|---|---|---|
| Alert Style | Ripple + Sound (default) Β· Ripple Only | Turns the ding off without hiding anything β the tab still pulses and shows its state. |
| Pet Size | Small Β· Medium (default) Β· Large | Resizes the whole floating panel live. |
| Re-nudge Interval | 1 / 2 (default) / 5 / 10 min Β· Never | How often a stuck needs_permission session re-alerts. |
| Quiet Hours | Off (default) Β· 10 PMβ8 AM Β· 11 PMβ7 AM Β· 9 PMβ9 AM | A daily window where alerts go quiet automatically. |
| Respect macOS Focus | On (default) Β· Off | Any Focus/Do Not Disturb mode silences alerts too β see caveat below. |
| Mute this session | per-tab, right-click a row | Stops alerting for just that one session; it stays visible with a π. |
Muting β per-session, Quiet Hours, or Focus β only silences the interruption (sound, ripple, auto-raising the window). The tab, its status dot, and the "stuck" pulse stay visible; Rocky's whole point is peripheral vision, so muting never makes a session invisible, only quiet.
Focus/Do Not Disturb sync is best-effort. macOS has no public API for "is Focus on right now" β Rocky reads the same undocumented file several menu-bar utilities use (
~/Library/DoNotDisturb/DB/Assertions.json), which requires Full Disk Access for Rocky and can change shape across macOS releases without notice. If it can't read the file, Rocky never assumes Focus is on (alerts still fire) and the right-click menu says so plainly ("β Focus sync needs Full Disk Access for Rocky") instead of silently doing nothing.
Homebrew:
rocky-teardown # remove Rocky's hooks from settings.json
brew services stop rocky
brew uninstall rockyFrom source:
./uninstall.shEither way this stops the agent, removes Rocky's files, and strips only
Rocky's hooks from settings.json (your other hooks are left intact).
- Warp: Rocky opens the session's
WARP_FOCUS_URL(warp://session/<uuid>), which Warp exports in every session's environment β it jumps to the exact tab. No permissions, no config. - iTerm2 / Terminal.app: fully scriptable β Rocky selects the exact tab by tty.
- kitty: exact window via remote control, when it's enabled
(
allow_remote_control yes+listen_on unix:/tmp/kitty-{kitty_pid}in kitty.conf). Without it, clicking activates kitty and the tab says so. - VS Code / Cursor: Rocky opens the session's folder, which these single-instance apps route to the window already showing that workspace.
- tmux (inside any terminal): Rocky selects the exact tmux window + pane, switches the attached client to the right session, then raises the hosting terminal β deep focus even in terminals with no scripting story of their own.
- Ghostty / Alacritty / everything else: no tab-scripting surface exists, so clicking activates the app. Rocky is honest about it: hovering the tab shows "click focuses <app> only". (Tip: run Claude inside tmux there and deep focus works.)
Every running Claude Code session appears automatically β Rocky reads
Claude's live session registry (~/.claude/sessions/), so it doesn't depend on
hooks firing. Sessions stay listed as long as their process is alive (even when
idle) and drop off when they exit. Hooks aren't needed for a session to appear;
they only enrich its status (tool names, needs-permission, your-turn alerts).
That registry is an undocumented Claude Code internal, so Rocky never trusts it blindly: a versioned adapter decodes it, tolerating renamed fields if the format drifts, and if it becomes unreadable entirely Rocky degrades to hooks-only mode (still showing every session that's fired a hook) instead of going blank. The right-click menu always shows the current read: "β Registry OK Β· N sessions" or a plain-language warning if something's off.
- Logs:
/tmp/rocky.logβ includes a self-check line on launch ("Hooks wired?", "Registry readable?") and whenever registry health changes. - Rocky reads
~/.claude/sessions/and~/.claude/rocky/sessions/locally and never sends anything off your machine. - Rocky never alerts for a state it finds already true the moment it (re)starts β only for things that change while it's running β so relaunching Rocky doesn't re-ding you for a session that's been waiting for an hour.
One main.swift (AppKit, no dependencies) plus a small Python hook. Worth a
read if you're curious how a Claude Code hook can drive a native macOS UI:
main.swiftβ the pet: transparent non-activating panel, a hand-drawn pixel cat rendered with Core Graphics, and the registryβ¨―hook merge.rocky-hook.pyβ maps Claude Code hook events to per-session state files.install.sh/uninstall.shβ build, install the login agent, and callscripts/wire-hooks.pyto wire/unwire the hooks insettings.json(idempotent; your other hooks are untouched).
MIT β see LICENSE.





