# slab A desk for running many coding agents at once. Slab is a macOS menu bar app (plus a handful of hooks and scripts) that gives every Claude Code, Codex or Easel session on the machine a colour, a name, a stone, and a place on the wall. Story page: . What the menu bar does today: - **Theme by status.** Each Terminal window is re-skinned by the session inside it: one tint while the agent works, another when it is waiting on you, a third when the turn is complete. Profiles live in `seed/`. - **Title by task.** The window title becomes what the session is doing. - **Prompt rocks.** One 3D pixel stone per live prompt, parked top-right of its terminal. Shape = identity, spin = status, pet name in bubble letters, a seeded percussion voice on hover, the session summary on click. Easel rocks double as a scannable code for the piece being made. (`menubar-swift/Sources/SlabMenubar/PromptSigilOverlay.swift`) - **The wall.** ⌘⌥T tiles the agent terminals, ⌘⌥S scatters them, ⌘⌥ arrows walk focus spatially (and across Macs over Deskflow), ⌘⌥X focuses the rocks, ⌃⌥⌘A flips dark mode fleet-wide. - **Status sounds.** Three synthesized cues (complete / awaiting / disconnected) that share one voice and differ only in contour. - **Game Mode.** GeForce NOW calls Tailscale a VPN on sight of the tunnel interface, even with no exit node routing a single packet. One switch pauses the tailnet and pins AirDrop's `awdl0` radio down (it channel-hops the Wi-Fi card mid-stream), and puts both back on the way out — optionally by itself, the moment GeForce NOW opens. (`menubar-swift/Sources/SlabMenubar/GameMode.swift`) - **Fleet.** Every Mac runs the same menu bar; they share a ledger of live sessions over the tailnet (`host:name`), `frame` photographs any desk and `puppet` acts on it. Rocks are never drawn for remote machines. Slab is not packaged: no download, no installer script. Clone the monorepo, run `./install.sh --prompt-host` for hooks + markers, then `menubar-swift/install.sh` to build and launch the bar. The rest of this README is mostly about the lid daemon, which is where Slab began. ## The lid (where it started) A lid-closed ambient + reactive audio system for the MacBook, gated on Claude Code activity, with a completion chime that sleeps the Mac when the last thread finishes. Close the lid while Claude has active work in flight (with sleep suppressed) and the laptop becomes a "sleeping slab": an ambient C-major-pentatonic bed loops through the speakers while a mic-listener picks up claps, snaps, shushes, kisses, hums, and close-up sings — responding in real time with short pentatonic pluck-arpeggios that mirror each sound's pitch contour. Close the lid with nothing active and it's silent. When Claude finishes a prompt, it beeps once per other in-flight prompt or subagent; when the last piece of work completes, the "all-done" chime plays — and if the lid is closed, the Mac sleeps right after. Active work is tracked as marker files in two directories: - `$SLAB_HOME/state/active-prompts/` — created by the `UserPromptSubmit` hook, removed by `Stop`. - `$SLAB_HOME/state/active-subagents/--` — created by the `PreToolUse` hook (filtered to the Task tool), removed (oldest-first) by `SubagentStop`. The daemon treats the total file count as "active work". When the Stop hook runs with zero remaining markers and the lid is closed, ambient stops, the all-done chime plays, and the Mac sleeps. ## What it does | Event | Sound | |---|---| | Lid close (`disablesleep=1`) | display sleep. Ambient only if a prompt or subagent is in flight. | | Prompt or subagent starts while lid closed | ascending C-arp + ambient loop starts | | Mic transient while ambient is playing (> 2 kHz) | 4-note pentatonic pluck arp, contour mirrors input | | Lid open while ambient playing | descending ding + smooth descending arp stinger + ambient stops | | Lid open with no ambient | silent | | Subagent finishes (Task tool) | single ping | | Claude Stop, **other active work remaining** | N ascending beeps (C6 D6 E6 G6 A6 C7 D7 E7) | | Claude Stop, all work done, lid open | "all-done" chime | | Claude Stop, all work done, lid closed | TTS "i'm tired" with cosine fade-out tail → `pmset sleepnow` | | User submits new prompt | touches active-prompts marker, sets `disablesleep=1` | ## Install ```sh git clone git@github.com:whistlegraph/aesthetic-computer.git cd aesthetic-computer/slab ./install.sh ``` The installer will: 1. Symlink scripts into `~/.local/bin/` 2. Copy sound assets into `~/.local/share/slab/sounds/` 3. Build a Python venv at `~/.local/share/slab/venv/` with `numpy`, `sounddevice`, and `rumps` 4. Install two launchd agents: - `~/Library/LaunchAgents/computer.slab.daemon.plist` — the lid-ambient daemon - `~/Library/LaunchAgents/computer.slab.menubar.plist` — a menu-bar status item + transparent full-screen HUD in the top-right corner of the screen 5. Merge hooks into `~/.claude/settings.json` (Stop, SubagentStop, PreToolUse(Task), UserPromptSubmit, SessionStart) 6. Install a passwordless-sudo rule at `/etc/sudoers.d/slab-pmset` for `pmset` (prompts for password once) Opt-outs: `--no-hooks`, `--no-sudoers`. For a Mac that should participate in the prompt-rock/prox fleet without the ambient audio, lid daemon, or sudoers changes, use: ```sh ./install.sh --prompt-host ``` This installs the agent wrappers, Claude hooks, and marker state only. Install the Swift menu bar separately with `menubar-swift/install.sh`. ## Two Slabs: the studio one and the shipped one `install.sh` builds the Slab that belongs to this studio. It symlinks scripts back into the checkout so an edit takes effect immediately, merges hooks that shell out to `jq`, and signs the bundle with a stable *self-signed* certificate — chosen deliberately, because ad-hoc signing changes the code hash every build and would revoke the Accessibility grant each time. `menubar-swift/release-solo.sh` builds the other one: the Slab a stranger can download. ```sh cd menubar-swift ./release-solo.sh # build → sign → notarize → staple → DMG ./release-solo.sh --no-notarize # local smoke test; the result will NOT open elsewhere ``` It produces `menubar-swift/dist/Slab-.dmg`, Developer ID-signed with the hardened runtime, notarized and stapled so it opens offline on a Mac that has never heard of us. Three things differ from the studio build: - **Hooks live in the binary.** The shell hooks need `jq` thirteen times and `python3` three times, and macOS ships neither. `slab-menubar hook ` (see `Sources/SlabMenubar/HookCLI.swift`) reimplements the marker protocol with `JSONSerialization` and `sysctl`, so the only dependency left is the app itself. Both halves write the same markers; change one shape and the rocks stop reading. - **It sets itself up.** On first launch `FirstRun.swift` offers to merge those hooks into `~/.claude/settings.json` and seed the Terminal profiles from a copy bundled in `Contents/Resources/solo`. It refuses outright when `~/.local/bin/claude-prompt-log.sh` is a symlink, which is how it knows a repo install owns the machine and must not be touched. - **It is signed by Developer ID.** A Mac holding both builds will be asked to grant Accessibility once more, because the designated requirement is part of what TCC remembers. Delete the old bundle when you switch, or the permission prompt returns on every launch. `bin/verify-solo-bundle.sh` gates the signing step. It checks the parts are present, that the binary references no path inside a home directory, that it links only system libraries, and that the `hook` subcommand really writes a marker. That check exists because Menu Band once shipped a DMG that ran on the machine that built it and crashed everywhere else. Requires a Mac holding the Developer ID key (blueberry or neo) and the vault's `apple/app-specific-password.env` for the notary service. **To publish**, follow the Menu Band pattern: upload the DMG to `assets.aesthetic.computer/slab/`, then record version, size and sha256 in a manifest the `/slab` page reads. Do not publish a build that has not been launched on a Mac that never ran Slab — the first-run dialog and the TCC prompts are the two things no check here can prove. ## Usage ```sh claude-sleep awake # disable all sleep (stay awake with lid closed) claude-sleep auto # restore normal sleep behavior claude-sleep now # sleep the Mac immediately claude-sleep status # show SleepDisabled state ``` Before using lid-closed ambient mode: `claude-sleep awake`. The first time the mic listener runs you'll get a macOS microphone permission prompt. ## Uninstall ```sh ./uninstall.sh # unload agent, remove symlinks + hooks + sudoers ./uninstall.sh --purge # also delete ~/.local/share/slab (sessions, logs, venv, sounds) ``` ## Layout ``` slab/ ├── bin/ # scripts (symlinked into ~/.local/bin/) │ ├── lid-ambient.sh # launchd daemon, polls lid + active prompts │ ├── lid-reactive.py # mic → pluck-arp synth + noise voice (Python) │ ├── lid-ambient-synth.swift # live AVAudioEngine ambient drone + capture (Swift) │ ├── lid-return-generate.py # generate lid-return.wav (smooth descending arp) │ ├── slab-menubar.py # rumps menu bar status item (no Dock icon) │ ├── claude-sleep # sleep-state toggle │ ├── claude-stop.sh # Stop-hook entry (prompts + subagents aware) │ ├── claude-prompt-log.sh # UserPromptSubmit hook (touches active-prompts marker) │ ├── claude-tool-pre.sh # PreToolUse(Task) hook (touches active-subagents marker) │ ├── claude-subagent-stop.sh # SubagentStop hook (pops oldest subagent marker + ping) │ ├── claude-ping-repeat.sh # legacy 30s pings (no longer invoked) │ ├── claude-sleep-schedule.sh # legacy delayed auto-sleep (no longer invoked) │ ├── slab-monitor.sh # resource sampler │ └── slab-zone # location-zone manager ├── sounds/ # WAV assets (lid chimes, pings, beeps) ├── launchd/ │ ├── computer.slab.daemon.plist.template │ └── computer.slab.menubar.plist.template ├── sudoers.d/ │ └── slab-pmset.template ├── settings-fragment.json # Claude Code hooks, merged on install ├── install.sh └── uninstall.sh ``` Runtime state lives under `~/.local/share/slab/`: ``` sessions/-.wav per-lid-close mix of Python listener output (noise + plucks) sessions/ambient-.wav per-lid-close recording of the live Swift ambient drone sessions/-.jsonl trigger events + location metadata logs/lidalive.log daemon transitions logs/reactive.log reactive listener triggers logs/resources.jsonl CPU/RSS samples every 15s logs/claude-stop.log Stop-hook activity config/zones.json geofenced zones + per-zone scale/dynamics state/active-prompts/ one file per Claude session with a prompt in flight state/active-subagents/ one file per in-flight Task-tool subagent state/last-location.json cached coords (if Location Services unreachable) venv/ Python venv for the reactive listener sounds/ installed sound assets (lid chimes, pings, beeps) ``` ## Zones (location-aware ambient) Each lid-close queries Core Location and picks the closest zone in `~/.local/share/slab/config/zones.json`. The matching zone overrides the reactive listener's scale, pitch mapping (`div_factor`), and amp. Coords + zone name are written into the session JSONL and appended to the WAV filename. ```sh brew install corelocationcli # one-time (install.sh can run it) # grant Location Services permission once slab-zone where # show current coords + zone slab-zone add home 150 # pin current spot as 'home', 150m radius slab-zone add studio 60 slab-zone list # dump zones.json slab-zone remove home ``` Available scales: `major_pentatonic`, `minor_pentatonic`, `blues`, `dorian`, `phrygian`, `lydian`, `whole_tone`, `chromatic`. Edit `zones.json` directly to tweak a zone's `scale`, `div_factor`, or `arp_amp`. ## Tuning Most knobs live at the top of each script: - **Ambient** — `bin/lid-ambient-synth.swift`: scale, note-gap range, duration/fade ranges, detuning, drone frequency, AU effects (reverb preset, delay time/feedback). Recompile via `swiftc -O -o ~/.local/bin/lid-ambient-synth bin/lid-ambient-synth.swift` (install.sh does this automatically). - **Reactive listener** — `bin/lid-reactive.py`: `HIGH_BAND`, `TRIGGER_RATIO`, `MIN_GAP`, `DIV_FACTOR`, `NOTE_DUR`, `PLUCK_TAIL`, `ARP_NOTES`, `ARP_AMP`. Scales live in `SCALE_INTERVALS`. - **Lid-poll interval** — `bin/lid-ambient.sh`: `POLL` (default 0.5s). - **Resource poll** — `bin/slab-monitor.sh`: `INTERVAL` (default 15s). Edits to files in `slab/bin/` take effect immediately because the installed copies are symlinks back into the repo. ## Requirements - macOS (Apple Silicon tested). Intel should work but the display-sleep-on-lid-close path depends on `pmset displaysleepnow`. - Homebrew, Python 3.11+, `jq`, `swiftc` (Xcode Command Line Tools). - Microphone permission for the reactive listener (prompted on first run). ## Notes - `pmset disablesleep 1` suppresses **all** sleep, including explicit `pmset sleepnow` — the sudoers rule lets the scheduler toggle it back off before suspending. - The reactive listener writes 44.1 kHz mono int16 WAVs of only its own output (the pluck arps). The looping ambient `afplay` and the chimes aren't captured — that would need a loopback audio device like BlackHole. - Session files accumulate forever by default. Purge periodically: `rm ~/.local/share/slab/sessions/*`. - Apple Silicon MacBooks have no accelerometer, gyroscope, or accessible CPU temp via public APIs — so shake/gesture and temperature-modulation paths aren't implemented here yet.