Workstation setup
Developer machine setup for macOS. Homebrew, Node, pnpm, Git, editor config.
Workstation setup
Purpose: Provides a complete macOS developer machine setup guide covering core tools, peripherals, voice input, mobile access, and verification for Framework projects.
Developer machine setup for Framework projects. Run through this when setting up a new Mac.
Summary
- Core stack — Homebrew, pnpm, Git, gh CLI, 1Password CLI, iTerm2 + tmux, Cursor, Claude Code, pandoc + typst
- Peripherals — Corsair Xeneon Edge touchscreen with a patched TouchscreenDriver fork, Elgato Stream Deck with the custom Xeneon Hotkey plugin for keystroke actions and the Shortcuts plugin for Apple Shortcut triggers
- Stream Deck buttons — Xeneon Hotkey handles all keyboard shortcuts (paste, copy, undo, etc.); a separate "Paste to iTerm" Apple Shortcut enables one-tap image paste into Claude Code inside tmux
- Voice input — Wispr Flow with
macos-mic-keepwarmto prevent USB mic startup delay - Mobile access — Moshi (Claude Code-optimized terminal with Mosh + webhook notifications) via Tailscale, tmux sessions
- Diagramming — D2, Excalidraw CLI, and Gemini API key for the
/diagramskill - launchd management — launchd-ui for browsing, managing, and editing macOS launchd agents locally
- Job monitoring —
job-wrapper.shcaptures structured JSON status for launchd jobs;job-status.shprints a CLI dashboard - Remote job monitoring — Healthchecks.io dead-man's-switch service for alerting when launchd jobs fail to run on schedule
- Tool adoption — 6-point checklist for evaluating new tools (update channel, cadence, maintainer risk, update policy, documentation, monitoring assignment)
- Restore path — All critical dotfiles and driver source documented in the Restore from Backup table; Time Machine covers the rest
- Verification — 15-point checklist covering every tool, peripheral, and workflow
Tool adoption checklist
Before adding any new tool to this workstation setup, answer these five questions and document the answers inline with the tool's install instructions.
- Update channel — Is it in Homebrew / pnpm / npm? If not, how do we get new versions?
- Update cadence — How often does it release? (Informs whether automation is worth it)
- Maintainer risk — Single maintainer or team? Active or dormant?
- Update policy — Pick one:
- Package manager (Homebrew, pnpm) — automatic via
brew upgrade/pnpm update update-tools.shwith 7-day lag — for CLI tools/binaries without a package manager- Manual — for GUI apps or tools with very low release cadence
- Package manager (Homebrew, pnpm) — automatic via
- Decision documented — Record the chosen policy inline in the tool's section below (look for the Update policy field)
- Monitoring assigned — Which monitoring layer tracks this tool's updates? Options: Renovate (npm/GitHub Actions) / Dependabot (CVEs) / NewReleases.io (upstream repos) / launchd job (local system) / N/A (SaaS with no local install, self-updating, or framework-managed)
When to use this checklist: Any time you install a new tool, add a new MCP server, or adopt a new CLI binary for the framework. The checklist is referenced from
CLAUDE.md§ Locked Decisions.
Prerequisites
- macOS 13 Ventura or later — Apple Silicon recommended
- Homebrew installed —
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Core development tools
Update policy: Package manager (Homebrew). All core tools are Homebrew-managed; brew upgrade keeps them current.
Monitoring: launchd brew-outdated (Homebrew packages) + Renovate (pnpm via package.json)
# Package manager
brew install pnpm
# Version control
brew install git gh
# 1Password CLI (secrets management -- see environments.md)
brew install --cask 1password 1password-cli
# Terminal
brew install --cask iterm2
brew install tmux reattach-to-user-namespace
# Editor
brew install --cask cursor
# PDF generation (used by /pdf skill)
brew install pandoc typst
Terminal (iTerm2 + tmux)
iTerm2 is the primary terminal. tmux enables parallel sub-agent work in separate panes.
- Open iTerm2
- Select a project profile — Each Claude Code project gets its own iTerm profile (auto-generated). Selecting one opens a tmux session named after the project and launches Claude Code automatically.
- Sub-agents use panes — Sub-agents can be run in parallel tmux panes when needed
Dynamic profile generation:
Project profiles are auto-generated by tools/scripts/generate-iterm-profiles.sh. Run it once after cloning the framework repo and again whenever you add a new Claude Code project under ~/Sites/.
# Generate profiles for all Claude Code projects in ~/Sites/
bash ~/Sites/framework/tools/scripts/generate-iterm-profiles.sh
# Preview without writing (dry run)
bash ~/Sites/framework/tools/scripts/generate-iterm-profiles.sh --dry-run
The script scans ~/Sites/ for directories containing CLAUDE.md or .claude/, then writes a single claude-projects.json to ~/Library/Application Support/iTerm2/DynamicProfiles/. iTerm2 picks up the file automatically — no restart required.
Each generated profile:
- Is named after the project directory (e.g.,
framework,my-app) - Has a deterministic GUID derived from the project name (re-running is safe — GUIDs never change)
- Has a deterministic tab color derived from the project name (one of 20 visually distinct colors, consistent across regenerations)
- Sets the working directory to the project root
- Runs
~/.claude/framework-session.sh <project-name> <project-path>on open
The startup script (~/.claude/framework-session.sh) handles:
- Prompting for a short session name or intent (e.g.,
ORC-368 tab colors) - Sanitizing the input into a tmux-safe session name, preserving casing (e.g.,
ORC-368-tab-colors) - Falling back to auto-increment from the project directory name if the user presses Enter without input (e.g.,
framework,framework-2,framework-3, up to 8 slots) - Always creating a new tmux session (never reattaches to existing sessions)
- Launching Claude Code inside the new tmux session
tmux config (~/.tmux.conf):
set -g mouse on
set -g set-titles on
set -g set-titles-string "#S"
set-hook -g after-new-session 'run-shell "bash ~/.tmux/set-session-color.sh"'
- Mouse scrolling —
set -g mouse onenables scroll wheel in tmux without needingCtrl-b [to enter copy mode first - Window titles —
set-titles onpasses the session name to the terminal emulator's title bar - Session colors —
after-new-sessionhook assigns a deterministic status bar color per session name so you can identify which project is active at a glance
tmux session color-coding (~/.tmux/set-session-color.sh):
Each new tmux session gets a unique status bar color derived from its name via a cksum hash. The same session name always produces the same color — switching between projects is visually distinct. No configuration needed; the hook fires automatically.
Reload after changes: tmux source-file ~/.tmux.conf
Key tmux commands:
tmux new -s work— Start a named sessiontmux attach -t work— Reattach to an existing sessionCtrl-b d— Detach from session (keeps it running)
Editor (Cursor)
Update policy: Manual. Cursor auto-updates itself via its built-in updater. Monitoring: N/A (self-updating GUI app)
Cursor settings sync automatically via your account — no manual config needed on a new Mac.
- Open Cursor
- Sign in — With the same account (Settings > Turn on Settings Sync)
- Extensions, keybindings, and preferences restore automatically
Settings location: ~/Library/Application Support/Cursor/User/settings.json
Key settings that sync with your account:
- Default terminal profile — Set to Claude Code (
zsh -ic claude) - Claude Code extension — Positioned in panel
- Auto-save enabled — Auto-fetch for git
- Markdown files — Open in preview by default
What does NOT sync (per-project, lives in git):
.mcp.json— MCP server config (gitignored, copy from.mcp.json.exampleand add API keys).claude/— Agent definitions, team compositions, settings (committed to framework repo)
Claude Code
Update policy: Package manager (npm). Update via npm install -g @anthropic-ai/claude-code. Anthropic team-maintained; frequent releases.
Monitoring: Renovate (npm)
Claude Code config is committed to the framework repo under .claude/ (agents, teams, settings).
On a new Mac:
- Install Claude Code —
npm install -g @anthropic-ai/claude-code - Clone the framework repo —
.claude/settings.json(permissions, MCP enablement) comes with it - Copy
.mcp.json.exampleto.mcp.json— Add your API keys - Install FFF MCP — Indexed file search for faster, cheaper AI sessions.
Update policy:
update-tools.shwith 7-day lag. Single maintainer (Dmitry Kovalenko); nightly releases only. Launchd job auto-updates at 3 am viajob-wrapper.shwith Healthchecks.io monitoring. Monitoring: NewReleases.io (dmtrKovalenko/fff.nvim) + launchdfff-mcp-update
The plist wraps# Install the binary (picks the newest release at least 7 days old) bash ~/Sites/framework/tools/scripts/update-tools.sh # Register with Claude Code (global, not per-project) claude mcp add -s user fff -- ~/.local/bin/fff-mcp # Set up nightly auto-updates (7-day lag policy, wrapped with job-wrapper.sh) ln -sf ~/Sites/framework/tools/scripts/com.rajababa.fff-mcp-update.plist ~/Library/LaunchAgents/ launchctl load ~/Library/LaunchAgents/com.rajababa.fff-mcp-update.plistupdate-tools.shwithjob-wrapper.shwhich captures structured JSON status to~/.local/share/job-status/and pings Healthchecks.io on success/failure. See Job monitoring. - Set up Linear API key — Get a personal API key from Linear Settings > API > Personal API keys. Add to shell profile:
export LINEAR_API_KEY=lin_api_... - Restore global Claude config — See Restore from Backup
launchd management (launchd-ui)
Update policy: Manual. GUI app — check GitHub releases periodically for updates.
launchd-ui is a modern GUI for browsing, managing, and editing macOS launchd agents and daemons. Built with Tauri v2 + React + shadcn/ui. Complements Healthchecks.io (ORC-384) — launchd-ui manages jobs locally, Healthchecks.io monitors them remotely.
Features:
- Browse/search/filter all agents and daemons
- Start/stop/restart jobs
- Create/edit/delete user agents
- Schedule with interval or calendar
- View logs, reveal plist in Finder
- System agents are read-only; user agents are fully editable
Install:
- Download the latest release from GitHub releases — use the
aarch64.dmgfor Apple Silicon - Open the DMG and drag
launchd-ui.appto/Applications/ - Clear the quarantine attribute (unsigned app):
xattr -cr /Applications/launchd-ui.app - Launch and verify it shows all agents from
~/Library/LaunchAgents/includingcom.rajababa.fff-mcp-update
| Field | Answer |
|---|---|
| Update channel | GitHub Releases (not in Homebrew/pnpm) |
| Update cadence | Active — v1.2.0 released March 2025, regular commits |
| Maintainer risk | Single maintainer (azu), but well-known OSS contributor with many popular projects |
| Update policy | Manual — GUI app, low update urgency, check GitHub releases periodically |
| Monitoring | NewReleases.io (azu/launchd-ui) |
Job monitoring (job-wrapper.sh + job-status.sh)
Two framework scripts provide structured monitoring for launchd jobs.
job-wrapper.sh
Wraps any scheduled launchd command to capture structured JSON status and optionally ping Healthchecks.io.
Usage in a plist:
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/ameet/Sites/framework/tools/scripts/job-wrapper.sh</string>
<string>--name</string>
<string>fff-mcp-update</string>
<string>--healthcheck</string>
<string>https://hc-ping.com/<UUID></string>
<string>--</string>
<string>/bin/bash</string>
<string>/path/to/actual-command.sh</string>
</array>
What it captures (written to ~/.local/share/job-status/<name>.json):
- Job name, timestamp, duration, exit code, status (success/failure)
- Last 5 lines of stdout/stderr
- Healthchecks.io ping URL (if configured)
- Rolling history of last 30 runs in
<name>-history.json
Flags:
| Flag | Required | Description |
|---|---|---|
--name <name> |
Yes | Job identifier (used for status file naming) |
--healthcheck <url> |
No | Healthchecks.io ping URL; pings on success, /fail on failure |
job-status.sh
CLI dashboard that reads all status files and prints a formatted table:
bash ~/Sites/framework/tools/scripts/job-status.sh
Job Last Run Duration Status
-------------- ------------------- -------- ------------------
fff-mcp-update 2026-03-20 03:00:00 12s ✓ success
Which jobs are wrapped
| Job | Wrapped | Healthchecks.io | Notes |
|---|---|---|---|
com.rajababa.fff-mcp-update |
Yes | Yes (a3ac0ac0) |
Scheduled daily at 3am |
com.rajababa.shadcn-diff |
Yes | No | Scheduled weekly (Sunday 4am) — shadcn/ui component drift detection |
com.rajababa.brew-outdated |
Yes | No | Scheduled weekly (Sunday 4:15am) — Homebrew outdated packages |
com.user.keep-mic-warm |
No | Yes (6b8fff6c) |
KeepAlive daemon — job-wrapper.sh is for run-once scheduled commands. Monitored by com.rajababa.keep-mic-warm-health which runs check-daemon-health.sh every 5 min (ORC-387). |
Daemon health monitoring (check-daemon-health.sh)
For KeepAlive daemons (persistent processes), job-wrapper.sh does not apply because the daemon never "completes." Instead, check-daemon-health.sh runs periodically as a separate launchd agent and checks whether the daemon process is alive.
Usage in a plist:
<string>/bin/bash</string>
<string>/Users/ameet/Sites/framework/tools/scripts/check-daemon-health.sh</string>
<string>--label</string>
<string>com.user.keep-mic-warm</string>
<string>--healthcheck</string>
<string>https://hc-ping.com/<UUID></string>
| Flag | Required | Description |
|---|---|---|
--label <label> |
Yes | launchd service label to check via launchctl list |
--healthcheck <url> |
Yes | Healthchecks.io ping URL; pings on success, /fail on failure |
Install the keep-mic-warm health check:
ln -sf ~/Sites/framework/tools/scripts/com.rajababa.keep-mic-warm-health.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.rajababa.keep-mic-warm-health.plist
Troubleshooting
| Problem | Fix |
|---|---|
| No status file after job runs | Check ~/.local/share/job-status/ exists; wrapper creates it on first run |
| Healthchecks.io not receiving pings | Verify --healthcheck URL in plist; test manually: curl -fsS https://hc-ping.com/<UUID> |
job-status.sh shows no jobs |
No jobs have run through the wrapper yet; trigger one manually |
| Job shows "failure" | Check stdout_tail/stderr_tail in the JSON file for error details |
Remote job monitoring (Healthchecks.io)
Update policy: N/A — SaaS, no local install. Always uses the latest hosted version.
Healthchecks.io is a dead-man's-switch monitoring service for scheduled jobs. Each monitored job pings a unique URL on completion. If the expected ping does not arrive on schedule, Healthchecks.io sends an alert. This is the remote complement to launchd-ui — launchd-ui manages jobs locally, Healthchecks.io monitors them remotely.
Free tier: 20 checks, no credit card required. Sufficient for our 2-3 custom launchd agents.
Account setup
Create account — Go to healthchecks.io and sign up (email or GitHub OAuth)
Create project — Name it "Workstation Jobs"
Create checks for each monitored launchd job:
Check name Period Grace Notes FFF MCP Update 24h 1h Matches the nightly update schedule in com.rajababa.fff-mcp-update.plistKeep Mic Warm 5m 5m Daemon liveness check via check-daemon-health.shevery 5 min (ORC-387)Copy ping URLs — Each check gets a unique URL in the format
https://hc-ping.com/<UUID>Configure notifications — At minimum, enable email notifications. Slack and push notifications are optional.
Record UUIDs — Store the ping UUIDs securely; they are needed for plist integration (ORC-385)
Ping protocol
The job-wrapper.sh script (ORC-360) wraps each launchd job and handles ping calls automatically:
| Event | Curl command | When |
|---|---|---|
| Start | curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECK_URL/start |
Job begins (optional, enables duration tracking) |
| Success | curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECK_URL |
Job exits with code 0 |
| Failure | curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECK_URL/fail |
Job exits with non-zero code |
The healthcheck URL is passed per-job via the --healthcheck argument in the launchd plist file.
How to unwind
- Delete checks and project at healthchecks.io
- Remove
--healthcheckarguments from any launchd plist files - Remove the Healthchecks.io section from this document
| Field | Answer |
|---|---|
| Update channel | SaaS — no local install, always latest |
| Update cadence | N/A (hosted service) |
| Maintainer risk | Low — established service by Peteris Caune, running since 2015, open source (BSD), self-hostable as fallback |
| Update policy | N/A — SaaS, no local updates |
| Monitoring | N/A (SaaS, no local install) |
Linear CLI tools
Update policy: Manual (framework-managed). The linear-api.ts script lives in this repo; updates ship with framework commits. No external dependency to track.
Monitoring: N/A (framework-managed, ships with commits)
Command-line access to Linear for issue creation and management. Used by the /new and /go commands and framework pipeline scripts.
linear-api wrapper
A shell wrapper that lets you run linear-api from any directory without remembering the full npx tsx invocation path.
Install:
# Create the wrapper
cat > ~/.local/bin/linear-api << 'WRAPPER'
#!/usr/bin/env bash
# Wrapper: invoke linear-api.ts from anywhere.
# Install: copy to ~/.local/bin/ and chmod +x
exec npx tsx ~/Sites/framework/tools/scripts/linear-api.ts "$@"
WRAPPER
chmod +x ~/.local/bin/linear-api
# Verify
linear-api
The wrapper should print the usage help text.
.linear.json config (child projects)
.linear.json lives at the framework repo root and contains all Linear UUIDs (team, statuses, assignee, projects). Child projects symlink to it instead of maintaining their own copy.
Setup for a child project:
cd ~/Sites/my-child-project
ln -sf ../framework/.linear.json .linear.json
The projects map in .linear.json uses the repo's directory name as the lookup key. To register a new child project:
- Add an entry to the
projectsobject in~/Sites/framework/.linear.json:"projects": { "framework": "d712c56d-d0ae-46ed-8450-496534b71ac4", "my-child-project": "<linear-project-uuid>" } - Create the symlink in the child project (see above)
- Verify:
cat .linear.jsonshould show the framework's config
The /new command reads this file automatically to populate team, assignee, and project fields.
Audio & dictation
Wispr Flow + USB/external microphone fix
Update policy: Manual. Wispr Flow auto-updates itself. macos-mic-keepwarm is a static binary with no releases to track (single install script).
Monitoring: N/A (self-updating / static binary)
Problem: On Apple Silicon Macs, macOS powers down microphone hardware when no app is actively using it. When using a non-built-in mic (USB camera, USB audio interface, or Bluetooth headset), activating Wispr Flow causes a 2-5 second delay before audio capture begins, clipping the start of your speech.
Fix: Install macos-mic-keepwarm to keep the mic hardware awake at all times.
# Install
curl -fsSL https://raw.githubusercontent.com/drewburchfield/macos-mic-keepwarm/master/install.sh | bash
Post-install:
- macOS will prompt — For microphone permission
- Go to System Settings — Privacy & Security > Microphone
- Find "mic-warm" — Toggle to Allow
What to expect:
- Orange dot in menu bar — Labeled "mic-warm", this is normal
- Negligible CPU and battery impact — ~0%
- Starts automatically on login — Via LaunchAgent
- Binary location —
~/.local/bin/mic-warm
Uninstall:
curl -fsSL https://raw.githubusercontent.com/drewburchfield/macos-mic-keepwarm/master/uninstall.sh | bash
Additional tips:
- Disable Siri voice trigger — System Settings > Siri & Spotlight; it can interfere with dictation
- Conferencing apps — Zoom, Teams install audio plugins in
/Library/Audio/Plug-Ins/HAL/that can increase mic activation latency
Source: macos-mic-keepwarm
Claude Code Remote Control (disabled)
Claude Code has a built-in Remote Control feature (enableRemoteConnections in ~/.claude/settings.json) that lets you connect to local sessions from claude.ai/code or the Claude mobile app via Anthropic's infrastructure.
This feature is intentionally disabled. Our standard remote access method is Moshi + Tailscale + tmux (see section below), which provides:
- Full terminal UI -- see exactly what Claude sees, scroll history, copy/paste
- Voice input -- on-device Whisper via Moshi keyboard long-press
- Resilient connections -- Mosh protocol survives network switches and sleep
- Push notifications -- moshi-hooks fires on task completion, tool use, errors, and approval prompts
- Parallel sub-agents -- tmux panes for parallel agent work
- No intermediary routing -- direct P2P connection via Tailscale, no Anthropic infrastructure
To verify the setting is disabled, check ~/.claude/settings.json:
"enableRemoteConnections": false
Or run /config in the Claude Code CLI and ensure "Enable Remote Control for all sessions" is set to false.
If someone re-enables this setting: It will be caught by the Locked Decisions table in CLAUDE.md. Revert to false and use Moshi + Tailscale + tmux instead.
Mobile remote access (SSH + Tailscale + tmux)
Update policy: Manual. Tailscale auto-updates itself. Mosh is Homebrew-managed (brew upgrade mosh).
Monitoring: N/A (Tailscale self-updates) + launchd brew-outdated (mosh via Homebrew)
Lets you SSH into your Mac from your iPhone over a secure private network, attach to a running tmux session, and monitor or control Framework / Claude Code from anywhere — slopes, couch, wherever. Claude Code keeps running on your Mac even when you disconnect.
One-time Mac setup
- Install Tailscale — Download from tailscale.com or the Mac App Store, sign in, and connect
- Tailscale lives in the menu bar — if you can't see it (hidden behind the notch), run
open -a Tailscalefrom terminal - Verify with
tailscale statusand note your Mac's Tailscale IP (e.g.100.92.49.45)
- Tailscale lives in the menu bar — if you can't see it (hidden behind the notch), run
- Enable SSH — System Settings -> General -> Sharing -> Remote Login -> ON
- Prevent Mac from sleeping — System Settings -> Battery -> Options -> disable automatic sleep when display is off
- Otherwise Claude Code stops mid-run
- Disable key expiry (prevents browser tab on reboot) — Go to login.tailscale.com/admin/machines, find your Mac, click
...menu, select Disable key expiry. Without this, Tailscale opens a browser tab on every reboot to re-authenticate. - Verify:
tailscale status # should show your Mac and iPhone both connected tailscale ip # shows your Mac's Tailscale IP
One-time iPhone setup
- Install Tailscale on iPhone — App Store -> Tailscale. Sign in with the same account as your Mac. Both devices should appear green in the Tailscale dashboard.
- Install Moshi on iPhone — See Moshi mobile terminal section below for full setup
Daily usage
Starting a session on your Mac:
tmux new -s framework # create a named session
# navigate to your project and start Claude Code as normal
From your iPhone (Moshi):
- Open Moshi — Tap your saved host -> auto-connects via Mosh
- List running sessions —
tmux ls - Attach to your session —
tmux attach -t framework(or justtmux attachif only one session) - You're now mirrored — Into the exact same terminal as your Mac in real time
Key tmux commands
| Action | Command |
|---|---|
| Detach (leave session running) | Ctrl+B, D |
| Reattach | tmux attach -t <name> |
| New window | Ctrl+B, C |
| Switch windows | Ctrl+B, 0-9 |
| List windows | Ctrl+B, W |
| List sessions | tmux ls |
Important notes
- tmux sessions don't survive Mac reboots — You'll need to start a new session after a reboot. Tailscale and SSH survive fine.
- Tailscale runs on boot automatically — Verify under System Settings -> General -> Login Items
- If the screen looks garbled on mobile — Detach and reattach to reset the display
- One session, two views — The screen reflows to fit your phone's narrower terminal. Both screens are the same live session
- Signal drops are fine — Just re-open Moshi and
tmux attach. Claude Code never stopped
Troubleshooting
| Problem | Fix |
|---|---|
| Can't see Tailscale in menu bar | Run open -a Tailscale in terminal |
| SSH connection refused | Check Remote Login is ON in System Settings -> Sharing |
| tmux session gone after reboot | Start a new one: tmux new -s framework |
| Screen looks garbled on phone | Detach and reattach: Ctrl+B, D then tmux attach |
| Tailscale not connecting | Check login.tailscale.com dashboard — both devices should show green |
Moshi mobile terminal (Claude Code-optimized)
Update policy: Manual. iOS App Store auto-updates. Single developer (FrontierOne Software); see Risks section below. Monitoring: N/A (iOS App Store auto-updates)
Purpose-built iPhone terminal for AI coding agents. Uses the Mosh protocol for resilient connections that survive network switches, sleep, and subway tunnels.
App: Moshi: SSH & MOSH Terminal by FrontierOne Software — Free (SSH) / $19.99/yr Pro (Mosh protocol)
Mac setup
# Install mosh server (required for Mosh protocol)
brew install mosh
# Verify
mosh --version
Mosh uses UDP ports 60000-61000. Over Tailscale this is a non-issue — Tailscale is a flat network with no NAT/firewall between peers.
iPhone setup
- Install Moshi — App Store, search "Moshi SSH" (by FrontierOne Software)
- Subscribe to Pro — Settings > Subscription > Moshi Pro Yearly ($19.99/yr) for Mosh protocol support
- Add host:
- Hostname — Your Mac's Tailscale IP (e.g.
100.92.49.45) - Username —
ameet - Auth — SSH key (import from iOS Keychain or generate new)
- Protocol — Mosh (requires Pro)
- Hostname — Your Mac's Tailscale IP (e.g.
- Set up Agent Hooks — In Moshi app: Settings > Agent Hooks > follow setup instructions
Agent Hooks (moshi-hooks)
Moshi has native Claude Code integration via the moshi-hooks npm package. This replaces manual webhook curls and provides:
- Live Activity widget on your iPhone lock screen (shows task status, model, context %)
- Audio prompts ("Peon Ping Pack") that play sounds on agent events
- Event coverage — fires on task completion, tool use, errors, and approval prompts
Setup:
- In Moshi app: Settings > Agent Hooks
- Copy the two commands displayed
- Run both on your Mac:
bunx moshi-hooks setup bunx moshi-hooks token <YOUR_TOKEN> - Hooks are automatically added to
~/.claude/settings.json - Test with the "Test Notification" button in the Moshi app
Audio configuration:
- Toggle audio prompts on/off in Moshi: Settings > Agent Hooks > Audio Prompts
- Select sound pack in Moshi: Settings > Agent Hooks > Select Sounds
Key features
- Claude Code keyboard — Dedicated Ctrl/Esc/Tab toolbar, tmux shortcuts panel (long-press Ctrl), Claude slash-command panel
- Voice-to-terminal — On-device Whisper speech recognition, no cloud, no latency. Long-press the keyboard toggle button.
- Mosh resilience — Survives network switches, sleep, subway tunnels. Session stays alive.
- Biometric auth — SSH keys stored in iOS Keychain, unlocked via Face ID/Touch ID
- Modifier locking — Double-tap Ctrl or Alt to lock; single-tap for one-shot
Daily usage
- Open Moshi — Tap your saved host → auto-connects via Mosh
- Attach to tmux —
tmux attach -t framework - Voice commands — Long-press keyboard toggle for dictation
- Notifications — Pocket your phone; moshi-hooks pushes Live Activity updates and audio prompts on agent events
Risks
- Single developer (FrontierOne Software). If development stops, SSH still works (free tier). Mosh is the feature behind the Pro paywall.
- Pro subscription required for Mosh — $19.99/yr. SSH is free and works fine over Tailscale.
- moshi-hooks npm dependency —
bunx moshi-hooksruns via npm. If the package is unavailable, notifications stop but terminal access is unaffected. Re-run setup to reconfigure.
How to unwind
- Remove moshi-hooks entries from
~/.claude/settings.json - Delete Moshi app from iPhone
brew uninstall mosh(optional — doesn't affect anything else)
Source: getmoshi.app
Hardware peripherals
Corsair Xeneon Edge (touchscreen display)
Update policy: Manual. Patched fork at ~/Sites/_forks/TouchscreenDriver-fix/; we maintain patches locally. No upstream release cadence to track.
Monitoring: N/A (local patched fork, we maintain)
The Corsair Xeneon Edge is a 2560x720 ultra-wide touchscreen mounted below the main monitor. macOS doesn't natively support its touch digitizer for click input — a custom driver converts touch events into mouse clicks mapped to the correct display coordinates.
Driver: TouchscreenDriver
The upstream driver has bugs on macOS Tahoe.
Use our patched fork at ~/Sites/_forks/TouchscreenDriver-fix/.
Patches applied (not upstream):
print()replaced withFileHandle.standardOutput.write()— Fixes log output under launchdNSApplication.sharedinitialized with.accessorypolicy — NSScreen returns live dataCFRunLoopRun()replaced withNSApplication.shared.run()— AppKit events dispatch properlyfindXeneonDisplayID()correlates viadeviceDescription["NSScreenNumber"]— Correct display mappingupdateScreenFromCurrentList()removed from HID callback — Stops geometry overwrite on every eventpendingX/pendingYbuffering — Handles HID element ordering race- Plist:
LimitLoadToSessionType: Aqua+ProcessType: Interactive— Runs in GUI session launchctl load/unloadreplaced withbootstrap/bootout— Correct API for macOS Tahoe- v3.0.0 cursor fix —
injectDragguarded by!suppressCursorEvents; prevents drag events from overriding the warp-back during the post-click suppression window. Without this, tapping Stream Deck buttons on the Xeneon Edge would move the main display cursor to the touch position (broken since Stream Deck v7.3.1) - v3.0.0 event mask widened — Suppression tap now intercepts
mouseMoved+leftMouseDragged+rightMouseDragged+otherMouseDragged(wasmouseMovedonly) - v3.0.0 all logs/comments translated to English — Was French
Install (from patched source):
cd ~/Sites/_forks/TouchscreenDriver-fix
# Compile
swiftc TouchscreenDriver.swift -o TouchscreenDriver \
-framework IOKit -framework CoreFoundation \
-framework CoreGraphics -framework AppKit -O
# Install binary
sudo cp TouchscreenDriver /usr/local/bin/
# Install LaunchAgent
cp com.ymlaine.touchscreendriver.plist ~/Library/LaunchAgents/
# Start
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ymlaine.touchscreendriver.plist
First-time permissions:
- Accessibility — System Settings -> Privacy & Security -> Accessibility -> allow TouchscreenDriver
- Input Monitoring — System Settings -> Privacy & Security -> Input Monitoring -> allow TouchscreenDriver
macOS setting required: System Settings -> Desktop & Dock -> "Click wallpaper to reveal desktop" -> Only in Stage Manager. Without this, touching the desktop triggers the "show desktop" animation.
Management commands:
# Status
launchctl print gui/$(id -u)/com.ymlaine.touchscreendriver
# Logs
tail -f /tmp/touchscreendriver.log
# Stop
launchctl bootout gui/$(id -u)/com.ymlaine.touchscreendriver
# Start
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ymlaine.touchscreendriver.plist
After recompiling: macOS invalidates Accessibility permissions when the binary hash changes.
Go to System Settings -> Privacy & Security -> Accessibility, remove TouchscreenDriver, then re-add /usr/local/bin/TouchscreenDriver.
Toggling off/on is not sufficient — you must fully remove and re-add.
Toolchain note: If compilation fails with "SDK is not supported by the compiler" (Swift version mismatch), reinstall Command Line Tools:
sudo rm -rf /Library/Developer/CommandLineTools
xcode-select --install
# Click "Install" in the dialog, wait for download to complete
Stream Deck (custom touch buttons)
Update policy: Manual. Stream Deck software auto-updates itself. Xeneon Hotkey plugin is maintained locally. Monitoring: N/A (self-updating)
Elgato Stream Deck provides programmable buttons on the Xeneon Edge's touch bar area. The Stream Deck software lets you assign actions (launch apps, run scripts, trigger shortcuts) to each button position.
Install:
- Download Stream Deck — From elgato.com/downloads
- Open and follow the setup wizard
- Grant any requested accessibility permissions
The Stream Deck buttons on the Xeneon Edge work via the touchscreen driver — the driver converts touch input to clicks, and Stream Deck's software handles the button actions.
Xeneon Edge focus limitation: When you tap a Stream Deck button on the Xeneon Edge, the touchscreen driver injects a mouse click at that position.
macOS shifts key focus to the Stream Deck window on the secondary display.
Stream Deck's built-in Hotkey action captures the keystroke target at click time — before the driver can restore focus — so the keystroke goes to Stream Deck's own Electron window instead of your app.
This is architecturally unfixable: Stream Deck uses CGEventPost which routes to the focused app, and the focus shifts before the button action fires.
Physical Stream Deck hardware doesn't have this problem because USB button presses never cause focus changes.
Solution: Use the Xeneon Hotkey plugin for all keystroke-based actions.
This custom plugin replaces Stream Deck's broken Hotkey with one that sends keystrokes via a bundled cgevent-key CLI tool after a 100ms delay (allowing the driver's focus restore to complete).
It appears natively in Stream Deck's action list with a configuration UI for key + modifiers.
Plugin location: ~/Library/Application Support/com.elgato.StreamDeck/Plugins/com.ameet.xeneon-hotkey.sdPlugin/
Also installed: The Shortcuts plugin (by SENTINELITE) for actions that need Apple Shortcuts integration (e.g., image paste to Claude Code). Ensure the Shortcuts app has Accessibility permission (System Settings > Privacy & Security > Accessibility).
[!NOTE] The OSA Script plugin (by Gabriel Perales) is abandonware — Intel-only, not notarized, confirmed broken on macOS Sequoia+. Do not use it.
Xeneon Hotkey buttons (paste, copy, undo, etc.)
Use the Xeneon Hotkey plugin for any keyboard shortcut button. It works identically to Stream Deck's built-in Hotkey but routes through the macOS event system correctly.
Setup:
- Find Xeneon Hotkey — In Stream Deck's action list (right sidebar)
- Drag Hotkey onto a button
- Configure settings — Select the Key (e.g., V) and check Modifiers (e.g., Command)
- Set a button icon/title
Common configurations:
| Button | Key | Modifiers |
|---|---|---|
| Paste | V | Command |
| Copy | C | Command |
| Cut | X | Command |
| Undo | Z | Command |
| Redo | Z | Command + Shift |
| Save | S | Command |
| Select All | A | Command |
| Find | F | Command |
How it works: On button press, the plugin calls cgevent-key (bundled Swift binary) which waits 100ms for the driver's focus restoration, then posts the keystroke via CGEventPost(.cghidEventTap).
The keystroke routes to the correctly focused app.
Universal paste button (Claude Code in tmux)
A single button that handles everything — images and text. The script auto-detects what's on the clipboard and sends the right keystroke:
| Clipboard content | Keystroke | Why |
|---|---|---|
| Raw image data (screenshot, Snagit single capture) | Ctrl+V |
Claude Code image paste |
| Multiple image files (Snagit multi-select, Finder) | Ctrl+V x N |
Loops through each image with dynamic delay |
| Single image file | Ctrl+V |
Claude Code image paste |
| Text / non-image content | Cmd+V |
Normal tmux paste |
For multiple images, the delay between pastes scales automatically by file size (0.5s base + 0.3s/MB, capped at 4s) — no manual tuning needed.
Setup:
- Open the Shortcuts app — Cmd+Space -> "Shortcuts"
- Create a new shortcut — Named "Paste to iTerm"
- Add a Run Shell Script action — With:
osascript /path/to/framework/tools/scripts/paste-images-to-iterm.applescript - In Stream Deck — Drag the Shortcuts > Launch Shortcut action onto a button
- Set the Shortcut dropdown — To "Paste to iTerm"
- Set a paste icon
[!IMPORTANT] Use Run Shell Script, not "Run AppleScript". The Shortcuts sandbox blocks the AppKit bridge (
use framework "AppKit") that the script needs for clipboard detection. Running viaosascriptbypasses this limitation.
Source:
tools/scripts/paste-images-to-iterm.applescript— uses the AppKitNSPasteboardAPI to detect clipboard content type and file URLs.
Supported image formats: PNG, JPG/JPEG, GIF, WebP, BMP, TIFF
Usage: Copy anything to clipboard, then tap the Stream Deck button.
Images paste inline via Ctrl+V; text pastes normally via Cmd+V.
Diagramming tools
Tools for generating and rendering diagrams alongside documentation.
D2
Update policy: Package manager (Homebrew). brew upgrade d2 keeps it current. Team-maintained (Terrastruct).
Monitoring: launchd brew-outdated (Homebrew)
Text-to-diagram language with sketch mode for hand-drawn aesthetics.
brew install d2
d2 --version
Generate a diagram from a .d2 file:
d2 input.d2 output.png --theme 200 --sketch
--sketchflag — Produces an Excalidraw-like hand-drawn aesthetic--theme 200— Selects a clean, neutral color scheme
Excalidraw CLI
Update policy: Package manager (npx). Runs via npx on each invocation, so it always fetches the latest version.
Monitoring: NewReleases.io (nicolo-ribaudo/excalidraw-cli)
Export Excalidraw files to PNG from the command line.
npx @nicolo-ribaudo/excalidraw-cli export input.excalidraw --output output.png
Gemini API key
Required for the /diagram skill (AI-generated diagrams).
- Get an API key — From Google AI Studio
- Add it to
~/.claude/settings.json— Underenv:
{
"env": {
"GEMINI_API_KEY": "your-key-here"
}
}
- Verify —
[ -n "$GEMINI_API_KEY" ] && echo "set" || echo "missing"
Dependency monitoring
Unified dependency update monitoring across 4 layers, all funneling into one weekly email digest. See ORC-389 for the full architecture.
Layer 1: Renovate Bot (npm + GitHub Actions)
Update policy: N/A — GitHub App, runs as a service on the repo. No local install. Monitoring: N/A (GitHub-hosted service)
Monitors all package.json dependencies and GitHub Action SHA pins. Creates a single pinned GitHub Issue titled "Dependency Dashboard" listing every outdated dep grouped by major/minor/patch. Zero PRs unless you check a checkbox on the dashboard issue.
Config: renovate.json at the repo root (committed to the repo).
Setup:
- Install the Renovate GitHub App on the
frameworkrepo - The
renovate.jsonconfig is already committed — Renovate reads it automatically - Configure GitHub notification preferences to receive weekly email digests for Renovate issues
| Field | Answer |
|---|---|
| Update channel | GitHub App — auto-updates, no action needed |
| Update cadence | Continuous (Mend.io team-maintained) |
| Maintainer risk | Low — backed by Mend.io (commercial company), large open-source community |
| Update policy | N/A — GitHub-hosted service |
| Monitoring | N/A (GitHub-hosted service) |
Layer 2: Dependabot Alerts (Security)
Update policy: N/A — built into GitHub, no local install. Monitoring: N/A (GitHub built-in feature)
Monitors known CVEs in the full dependency tree (including transitive deps). GitHub scans the lockfile against the advisory database. Alerts appear in the GitHub Security tab.
Setup:
- Go to repo Settings > Code security and analysis
- Enable Dependabot alerts
- Optionally enable Dependabot security updates for auto-PRs on critical CVEs
- Configure GitHub notification preferences to receive weekly email digests for security alerts
Layer 3: NewReleases.io (Upstream Repos)
Update policy: N/A — SaaS, no local install. Always uses the latest hosted version. Monitoring: N/A (SaaS, no local install)
Monitors GitHub repos for new releases/tags. Sends email digests (configurable: daily, weekly, or instant).
Repos to monitor:
| Repo | Purpose |
|---|---|
jnsahaj/tweakcn |
Color theme presets (43 themes) |
dmtrKovalenko/fff.nvim |
FFF MCP binary |
azu/launchd-ui |
launchd GUI |
shadcn-ui/ui |
Component registry |
nicolo-ribaudo/excalidraw-cli |
Diagram export |
anthropics/claude-code-action |
PR review action (backup to Renovate) |
Setup:
- Create account at newreleases.io
- Add the 6 repos listed above
- Set digest frequency to weekly
- Configure email notifications
| Field | Answer |
|---|---|
| Update channel | SaaS — no local install |
| Update cadence | N/A (hosted service) |
| Maintainer risk | Low — established service by Nikola Duza, running since 2019, free tier sufficient |
| Update policy | N/A — SaaS, no local updates |
| Monitoring | N/A (SaaS, no local install) |
Layer 4: launchd jobs (shadcn-diff + brew-outdated)
Two weekly launchd jobs that detect drift in local system tools.
shadcn-diff
Runs npx shadcn@latest diff from tools/prototype-scaffold/ to detect which of the 32 installed shadcn/ui components have upstream changes.
Install:
ln -sf ~/Sites/framework/tools/scripts/com.rajababa.shadcn-diff.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.rajababa.shadcn-diff.plist
Schedule: Weekly — Sunday at 4:00 AM
Manual run:
bash ~/Sites/framework/tools/scripts/job-wrapper.sh --name shadcn-diff -- \
bash -c "cd ~/Sites/framework/tools/prototype-scaffold && npx shadcn@latest diff 2>&1"
brew-outdated
Runs brew outdated --verbose to show which Homebrew packages have newer versions available.
Install:
ln -sf ~/Sites/framework/tools/scripts/com.rajababa.brew-outdated.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.rajababa.brew-outdated.plist
Schedule: Weekly — Sunday at 4:15 AM
Manual run:
bash ~/Sites/framework/tools/scripts/job-wrapper.sh --name brew-outdated -- \
/opt/homebrew/bin/brew outdated --verbose
Results: Both jobs write to ~/.local/share/job-status/. Check via job-status.sh:
bash ~/Sites/framework/tools/scripts/job-status.sh
Weekly email digest configuration
All 4 layers converge into a single weekly awareness email:
| Layer | Email Source | How to Configure |
|---|---|---|
| Renovate Bot | GitHub notification digest | GitHub > Settings > Notifications > Email preferences > set to weekly |
| Dependabot Alerts | GitHub notification digest | Same setting as Renovate — both use GitHub notifications |
| NewReleases.io | Built-in weekly digest | NewReleases.io > Settings > Notifications > set frequency to weekly |
| launchd jobs | CLI dashboard (manual check) | Run job-status.sh during weekly review; or add Healthchecks.io checks for automated alerting |
Restore from backup
These files live outside the framework repo. Time Machine backs them up automatically. On a fresh Mac restore, verify they are present. On a clean install without restore, recreate manually.
| File | Purpose | How to Recreate |
|---|---|---|
~/.claude/settings.json |
Global permissions, env vars, teammateMode, statusline config | Copy from a teammate or recreate from memory — no secrets |
~/.claude/framework-session.sh |
tmux session launcher with interactive name prompt | See Terminal (iTerm2 + tmux) — ~70 lines of shell |
~/.claude/statusline-command.sh |
Custom statusline script (branch, tokens, context bar) | Recreate — it's ~55 lines of shell |
~/.claude/mcp.json |
Global MCP servers (Linear, Supabase, Playwright) | Re-add via claude mcp add and re-authenticate. Credentials are in 1Password |
~/.tmux.conf |
tmux config (mouse + clipboard) | See Terminal (iTerm2 + tmux) for full config |
~/.tmux/set-session-color.sh |
tmux session color-coding hook | See Terminal (iTerm2 + tmux) — 6-line bash script |
~/.zshrc |
Shell config (PATH, aliases) | Restore from Time Machine or recreate |
~/.local/bin/linear-api |
Linear CLI wrapper | See Linear CLI tools — 4-line shell script |
~/Sites/_forks/TouchscreenDriver-fix/ |
Patched Xeneon Edge touch driver | Clone from upstream + apply patches (see Hardware Peripherals section) |
~/scratch/ |
Conversational session artifacts (brainstorms, diagrams, notes) | mkdir -p ~/scratch/ -- contents are ephemeral, no restore needed |
Verification
After setup, confirm:
-
pnpm --versionreturns a version -
gh auth statusshows authenticated -
op account listshows your 1Password account - iTerm2 opens and
tmux new -s teststarts a session - Dynamic profiles generated:
bash ~/Sites/framework/tools/scripts/generate-iterm-profiles.sh --dry-runlists at least one project -
claude-projects.jsonis valid JSON:python3 -m json.tool "$HOME/Library/Application Support/iTerm2/DynamicProfiles/claude-projects.json" - Project profiles visible in iTerm2 > Profiles (may need to open Profiles window to refresh)
- Selecting a project profile prompts for a session name, then opens the correct working directory and launches Claude Code in tmux
- Pressing Enter without input at the session name prompt uses auto-increment (e.g.,
framework,framework-2) - tmux status bar changes color on new session creation (verify
~/.tmux/set-session-color.shis executable) - Cursor Settings Sync is enabled and signed in
- Claude Code is installed (
claude --version) and.mcp.jsonis configured -
pandoc --versionandtypst --versionreturn versions - Wispr Flow activates instantly with external mic (no clipping)
-
tailscale statusshows your Mac connected (key expiry disabled — no browser tab on reboot) -
mosh --versionreturns a version -
~/scratch/directory exists - Moshi app connects to Mac via Mosh over Tailscale
- moshi-hooks configured (
bunx moshi-hooks setup+ token) and test notification received from Moshi app - Claude Code keyboard shortcuts work in Moshi (Ctrl+C, tmux panel via long-press Ctrl)
- TouchscreenDriver running (
pgrep -f TouchscreenDriver) and log at/tmp/touchscreendriver.logshows "Driver active!" - Stream Deck software running and buttons respond to touch
- Stream Deck image paste button sends image to Claude Code in tmux
-
d2 --versionreturns a version -
GEMINI_API_KEYis set ([ -n "$GEMINI_API_KEY" ] && echo set) -
which fff-mcpreturns~/.local/bin/fff-mcpandcat ~/.local/share/fff-mcp/version.txtshows a release tag -
linear-apiprints usage help (linear-apiwith no args) - launchd-ui launches and shows agents from
~/Library/LaunchAgents/includingcom.rajababa.fff-mcp-update -
com.rajababa.fff-mcp-update.plistusesjob-wrapper.shwith--healthcheckflag (verify withplutil -p ~/Library/LaunchAgents/com.rajababa.fff-mcp-update.plist) -
bash ~/Sites/framework/tools/scripts/job-status.shshows fff-mcp-update in the table (after at least one run) - Healthchecks.io account exists with "Workstation Jobs" project and checks configured (FFF MCP Update, Keep Mic Warm)
-
renovate.jsonexists at repo root (verify withplutil -lint renovate.jsonfrom repo root) - Renovate GitHub App installed on the repo (check repo Settings > GitHub Apps)
- Dependabot Alerts enabled (check repo Settings > Code security and analysis)
- NewReleases.io account exists with 6 upstream repos subscribed and weekly digest configured
-
com.rajababa.shadcn-diff.plistsymlinked:plutil -p ~/Library/LaunchAgents/com.rajababa.shadcn-diff.plist -
com.rajababa.brew-outdated.plistsymlinked:plutil -p ~/Library/LaunchAgents/com.rajababa.brew-outdated.plist -
bash ~/Sites/framework/tools/scripts/job-status.shshows shadcn-diff and brew-outdated in the table (after at least one manual run)
Related standards
standards/reference/environments.md— Environment strategy and secrets managementstandards/reference/new-project-checklist.md— Per-project setup after workstation is ready
Last updated: 2026-03-20 (ORC-385: job-wrapper.sh wiring + docs)