| name | kitty-session-persistence |
| description | Snapshot and restore kitty terminal sessions on macOS. Use when surviving WindowServer hangs without losing tabs, configuring snapshots, or restoring after a force-reboot. |
| user-invocable | false |
| allowed-tools | Bash(kitty *), Bash(kitten *), Bash(launchctl *), Bash(uname *), Bash(plutil *), Bash(test *), Read, Write, Edit, Grep, Glob |
| created | "2026-05-03T00:00:00.000Z" |
| modified | "2026-05-09T00:00:00.000Z" |
| reviewed | "2026-05-03T00:00:00.000Z" |
Kitty Session Persistence (macOS)
When to Use This Skill
| Use this skill when... | Use something else when... |
|---|
| Terminal sessions need to survive WindowServer hangs and force-reboots | The user's terminal is iTerm2 / Terminal.app / Ghostty / Wezterm โ kitty-specific |
Configuring kitty's listen_on socket and a snapshot LaunchAgent | Cross-platform terminal config โ this is macOS LaunchAgents |
| Restoring a previous session after a crash | One-off "open these tabs" โ kitty @ launch directly is enough |
| Auditing whether snapshot capture is still running | General system diagnostics โ see macos-incident-postmortem |
Platform Guard
This skill is macOS-only. All commands assume Darwin and launchctl-managed LaunchAgents. Refuse to act if uname -s is not Darwin.
test "$(uname -s)" = "Darwin" || { echo "macos-plugin: not Darwin, refusing"; exit 1; }
Core Expertise
Kitty exposes a remote control protocol via a Unix socket. With listen_on configured, kitten @ ls returns a JSON description of every OS window, tab, and window inside kitty โ enough to reconstruct the layout from scratch.
The pattern this skill installs:
- Always-listen socket โ
listen_on unix:/tmp/kitty-$USER in kitty.conf so the snapshot script can always reach the running instance.
- Snapshot script โ captures
kitten @ ls output to a timestamped JSON file under ~/.local/state/kitty-sessions/, with a latest.json symlink.
- LaunchAgent โ runs the snapshot script every 5 minutes (default) so the worst-case loss is one window of work.
- Restore command โ reads
latest.json and re-launches tabs with the recorded cwd and command.
Architectural notes
- Kitty's remote control requires
allow_remote_control yes in kitty.conf or --allow-remote-control yes at launch.
- The socket path must match between
listen_on and the kitten @ --to flag the snapshot script uses.
- LaunchAgent
StandardOutPath and StandardErrorPath are essential โ failing snapshots are otherwise silent.
- Use
RunAtLoad: false and rely on StartInterval so a kitty restart doesn't spawn a backlog of immediate snapshots.
Setup
1. Configure kitty
Add to ~/.config/kitty/kitty.conf:
allow_remote_control yes
listen_on unix:/tmp/kitty-{kitty_pid}
Using {kitty_pid} makes each kitty instance addressable on its own socket. For the persistent-socket pattern this skill needs, use the user-stable form instead:
allow_remote_control yes
listen_on unix:/tmp/kitty-USER
Replace USER with the actual username. The snapshot LaunchAgent points at this exact path.
Reload kitty after editing:
kitty @ --to unix:/tmp/kitty-USER load-config
2. Install the snapshot script
Create ~/.local/bin/kitty-snapshot (mode 0755):
#!/usr/bin/env bash
set -uo pipefail
[ "$(uname -s)" = "Darwin" ] || exit 0
SOCKET="unix:/tmp/kitty-${USER}"
STATE_DIR="${HOME}/.local/state/kitty-sessions"
mkdir -p "$STATE_DIR"
ts="$(date -u +%Y%m%dT%H%M%SZ)"
out="${STATE_DIR}/${ts}.json"
if ! kitten @ --to "$SOCKET" ls >"$out" 2>>"${STATE_DIR}/snapshot.err"; then
rm -f "$out"
exit 0
fi
ln -sfn "$out" "${STATE_DIR}/latest.json"
ls -1t "$STATE_DIR"/*.json 2>/dev/null | tail -n +4033 | xargs -r rm -f
The exit 0 on kitten @ ls failure is intentional โ the LaunchAgent fires every 5 minutes regardless of whether kitty is running.
3. Install the LaunchAgent
Write ~/Library/LaunchAgents/com.<reverse-dns>.kitty-snapshot.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.example.kitty-snapshot</string>
<key>ProgramArguments</key>
<array>
<string>/Users/USER/.local/bin/kitty-snapshot</string>
</array>
<key>StartInterval</key>
<integer>300</integer>
<key>RunAtLoad</key>
<false/>
<key>StandardOutPath</key>
<string>/Users/USER/.local/state/kitty-sessions/launchd.out</string>
<key>StandardErrorPath</key>
/Users/USER/.local/state/kitty-sessions/launchd.err
Replace com.example with a stable reverse-DNS prefix and USER with the literal username (LaunchAgents do not expand $HOME).
Validate, load, and verify:
plutil -lint ~/Library/LaunchAgents/com.example.kitty-snapshot.plist
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.example.kitty-snapshot.plist
launchctl print "gui/$(id -u)/com.example.kitty-snapshot" | grep -E 'state|last exit'
bootstrap replaces the deprecated launchctl load; on macOS 11+ prefer it.
4. Confirm capture is working
After ~5 minutes:
ls -lt ~/.local/state/kitty-sessions/*.json | head -3
There should be JSON files newer than the LaunchAgent install time. The newest file's content should match what kitty has open right now.
Restore
Read latest.json and re-launch each window/tab. A minimal restore script:
#!/usr/bin/env bash
set -euo pipefail
[ "$(uname -s)" = "Darwin" ] || exit 1
LATEST="${HOME}/.local/state/kitty-sessions/latest.json"
SOCKET="unix:/tmp/kitty-${USER}"
[ -L "$LATEST" ] || { echo "no snapshot found"; exit 1; }
jq -c '.[] | .tabs[] | .windows[] | {cwd, foreground_processes}' "$LATEST" |
while IFS= read -r win; do
cwd=$(printf '%s' "$win" | jq -r '.cwd')
cmd=$(printf '%s' "$win" | jq -r '.foreground_processes[0].cmdline | join(" ")')
kitten @ --to "$SOCKET" launch --type=tab --cwd="$cwd" "$cmd"
done
The shape of kitten @ ls output is [{tabs:[{windows:[{cwd, foreground_processes:[{cmdline}]}]}]}]. Adjust the jq query if a future kitty release changes it.
Common Patterns
Audit recent snapshots
ls -1t ~/.local/state/kitty-sessions/*.json | head -10
jq '. | length, [.[].tabs | length] | add' ~/.local/state/kitty-sessions/latest.json
Confirm LaunchAgent is healthy
launchctl print "gui/$(id -u)/com.example.kitty-snapshot" | \
grep -E 'state|last exit|on demand'
state = running (briefly, every 5 minutes) and last exit code = 0 indicate health. Persistent non-zero exit means something is broken โ check launchd.err.
Disable / unload temporarily
launchctl bootout "gui/$(id -u)/com.example.kitty-snapshot"
bootout is the modern equivalent of launchctl unload.
Snapshot interval tuning
| Interval | When to use |
|---|
60s (StartInterval=60) | Heavy editing days โ minimum loss-on-crash |
| 300s (default) | Balanced โ typical workflow |
| 900s (15 min) | Stable, low-edit days โ less noise in ls -lt |
Reload after editing the plist:
launchctl bootout "gui/$(id -u)/com.example.kitty-snapshot"
launchctl bootstrap "gui/$(id -u)" ~/Library/LaunchAgents/com.example.kitty-snapshot.plist
Agentic Optimizations
| Context | Command |
|---|
| Snapshot count last hour | find ~/.local/state/kitty-sessions -name '*.json' -newermt '1 hour ago' | wc -l |
| Most recent snapshot age | stat -f '%Sm %N' ~/.local/state/kitty-sessions/latest.json |
| Validate a plist | plutil -lint ~/Library/LaunchAgents/com.example.kitty-snapshot.plist |
| Last exit code | launchctl print "gui/$(id -u)/com.example.kitty-snapshot" | awk '/last exit code/{print $NF}' |
| Live socket reachable | kitten @ --to unix:/tmp/kitty-$USER ls | jq '. | length' |
Quick Reference
LaunchAgent commands (macOS 11+)
| Operation | Command |
|---|
| Load (modern) | launchctl bootstrap "gui/$(id -u)" <plist> |
| Unload (modern) | launchctl bootout "gui/$(id -u)/<label>" |
| Inspect | launchctl print "gui/$(id -u)/<label>" |
| Run now | launchctl kickstart -k "gui/$(id -u)/<label>" |
| List user agents | launchctl print-disabled "gui/$(id -u)" |
Kitty remote control
| Operation | Command |
|---|
| List sessions | kitten @ --to <socket> ls |
| Launch tab | kitten @ --to <socket> launch --type=tab --cwd=<path> <cmd> |
| Reload config | kitten @ --to <socket> load-config |
| Send text | kitten @ --to <socket> send-text --match "title:foo" "ls\n" |
Snapshot file structure
~/.local/state/kitty-sessions/
โโโ 20260503T120000Z.json
โโโ 20260503T120500Z.json
โโโ ...
โโโ latest.json -> 20260503T143000Z.json
โโโ snapshot.err
โโโ launchd.out
โโโ launchd.err
Error Handling
| Symptom | Cause | Fix |
|---|
kitten @ ls fails with "remote control not allowed" | allow_remote_control no in kitty.conf | Set to yes, reload config |
LaunchAgent loads but last exit code = 78 | Script not executable | chmod +x ~/.local/bin/kitty-snapshot |
| LaunchAgent loads but never fires | StartInterval missing or zero | Set to a positive integer (seconds) |
All snapshots empty [] | No kitty windows running, or wrong socket path | Check listen_on path matches --to |
bootstrap fails with "already loaded" | Old load form still active | launchctl bootout first, then bootstrap |
| Restore opens windows with wrong shell | cmdline[0] is the shell, not the user command | Use cwd only and let the user re-run |