Skip to main content
Reference

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

  1. Core stack — Homebrew, pnpm, Git, gh CLI, 1Password CLI, iTerm2 + tmux, Cursor, Claude Code, pandoc + typst
  2. 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
  3. 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
  4. Voice input — Wispr Flow with macos-mic-keepwarm to prevent USB mic startup delay
  5. Mobile access — Moshi (Claude Code-optimized terminal with Mosh + webhook notifications) via Tailscale, tmux sessions
  6. Diagramming — D2, Excalidraw CLI, and Gemini API key for the /diagram skill
  7. launchd management — launchd-ui for browsing, managing, and editing macOS launchd agents locally
  8. Job monitoringjob-wrapper.sh captures structured JSON status for launchd jobs; job-status.sh prints a CLI dashboard
  9. Remote job monitoring — Healthchecks.io dead-man's-switch service for alerting when launchd jobs fail to run on schedule
  10. Tool adoption — 6-point checklist for evaluating new tools (update channel, cadence, maintainer risk, update policy, documentation, monitoring assignment)
  11. Restore path — All critical dotfiles and driver source documented in the Restore from Backup table; Time Machine covers the rest
  12. 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.

  1. Update channel — Is it in Homebrew / pnpm / npm? If not, how do we get new versions?
  2. Update cadence — How often does it release? (Informs whether automation is worth it)
  3. Maintainer risk — Single maintainer or team? Active or dormant?
  4. Update policy — Pick one:
    • Package manager (Homebrew, pnpm) — automatic via brew upgrade / pnpm update
    • update-tools.sh with 7-day lag — for CLI tools/binaries without a package manager
    • Manual — for GUI apps or tools with very low release cadence
  5. Decision documented — Record the chosen policy inline in the tool's section below (look for the Update policy field)
  6. 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.

  1. Open iTerm2
  2. 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.
  3. 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 scrollingset -g mouse on enables scroll wheel in tmux without needing Ctrl-b [ to enter copy mode first
  • Window titlesset-titles on passes the session name to the terminal emulator's title bar
  • Session colorsafter-new-session hook 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 session
  • tmux attach -t work — Reattach to an existing session
  • Ctrl-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.

  1. Open Cursor
  2. Sign in — With the same account (Settings > Turn on Settings Sync)
  3. 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.example and 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:

  1. Install Claude Codenpm install -g @anthropic-ai/claude-code
  2. Clone the framework repo.claude/settings.json (permissions, MCP enablement) comes with it
  3. Copy .mcp.json.example to .mcp.json — Add your API keys
  4. Install FFF MCP — Indexed file search for faster, cheaper AI sessions. Update policy: update-tools.sh with 7-day lag. Single maintainer (Dmitry Kovalenko); nightly releases only. Launchd job auto-updates at 3 am via job-wrapper.sh with Healthchecks.io monitoring. Monitoring: NewReleases.io (dmtrKovalenko/fff.nvim) + launchd fff-mcp-update
    # 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.plist
    
    The plist wraps update-tools.sh with job-wrapper.sh which captures structured JSON status to ~/.local/share/job-status/ and pings Healthchecks.io on success/failure. See Job monitoring.
  5. 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_...
  6. 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:

  1. Download the latest release from GitHub releases — use the aarch64.dmg for Apple Silicon
  2. Open the DMG and drag launchd-ui.app to /Applications/
  3. Clear the quarantine attribute (unsigned app):
    xattr -cr /Applications/launchd-ui.app
    
  4. Launch and verify it shows all agents from ~/Library/LaunchAgents/ including com.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/&lt;UUID&gt;</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

  1. Create account — Go to healthchecks.io and sign up (email or GitHub OAuth)

  2. Create project — Name it "Workstation Jobs"

  3. 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.plist
    Keep Mic Warm 5m 5m Daemon liveness check via check-daemon-health.sh every 5 min (ORC-387)
  4. Copy ping URLs — Each check gets a unique URL in the format https://hc-ping.com/<UUID>

  5. Configure notifications — At minimum, enable email notifications. Slack and push notifications are optional.

  6. 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

  1. Delete checks and project at healthchecks.io
  2. Remove --healthcheck arguments from any launchd plist files
  3. 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:

  1. Add an entry to the projects object in ~/Sites/framework/.linear.json:
    "projects": {
      "framework": "d712c56d-d0ae-46ed-8450-496534b71ac4",
      "my-child-project": "<linear-project-uuid>"
    }
    
  2. Create the symlink in the child project (see above)
  3. Verify: cat .linear.json should 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:

  1. macOS will prompt — For microphone permission
  2. Go to System Settings — Privacy & Security > Microphone
  3. 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

  1. 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 Tailscale from terminal
    • Verify with tailscale status and note your Mac's Tailscale IP (e.g. 100.92.49.45)
  2. Enable SSH — System Settings -> General -> Sharing -> Remote Login -> ON
  3. Prevent Mac from sleeping — System Settings -> Battery -> Options -> disable automatic sleep when display is off
    • Otherwise Claude Code stops mid-run
  4. 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.
  5. Verify:
    tailscale status       # should show your Mac and iPhone both connected
    tailscale ip           # shows your Mac's Tailscale IP
    

One-time iPhone setup

  1. 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.
  2. 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):

  1. Open Moshi — Tap your saved host -> auto-connects via Mosh
  2. List running sessionstmux ls
  3. Attach to your sessiontmux attach -t framework (or just tmux attach if only one session)
  4. 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

  1. Install Moshi — App Store, search "Moshi SSH" (by FrontierOne Software)
  2. Subscribe to Pro — Settings > Subscription > Moshi Pro Yearly ($19.99/yr) for Mosh protocol support
  3. Add host:
    • Hostname — Your Mac's Tailscale IP (e.g. 100.92.49.45)
    • Usernameameet
    • Auth — SSH key (import from iOS Keychain or generate new)
    • Protocol — Mosh (requires Pro)
  4. 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:

  1. In Moshi app: Settings > Agent Hooks
  2. Copy the two commands displayed
  3. Run both on your Mac:
    bunx moshi-hooks setup
    bunx moshi-hooks token <YOUR_TOKEN>
    
  4. Hooks are automatically added to ~/.claude/settings.json
  5. 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

  1. Open Moshi — Tap your saved host → auto-connects via Mosh
  2. Attach to tmuxtmux attach -t framework
  3. Voice commands — Long-press keyboard toggle for dictation
  4. 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 dependencybunx moshi-hooks runs via npm. If the package is unavailable, notifications stop but terminal access is unaffected. Re-run setup to reconfigure.

How to unwind

  1. Remove moshi-hooks entries from ~/.claude/settings.json
  2. Delete Moshi app from iPhone
  3. 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 with FileHandle.standardOutput.write() — Fixes log output under launchd
  • NSApplication.shared initialized with .accessory policy — NSScreen returns live data
  • CFRunLoopRun() replaced with NSApplication.shared.run() — AppKit events dispatch properly
  • findXeneonDisplayID() correlates via deviceDescription["NSScreenNumber"] — Correct display mapping
  • updateScreenFromCurrentList() removed from HID callback — Stops geometry overwrite on every event
  • pendingX/pendingY buffering — Handles HID element ordering race
  • Plist: LimitLoadToSessionType: Aqua + ProcessType: Interactive — Runs in GUI session
  • launchctl load/unload replaced with bootstrap/bootout — Correct API for macOS Tahoe
  • v3.0.0 cursor fixinjectDrag guarded 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 (was mouseMoved only)
  • 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:

  1. Download Stream Deck — From elgato.com/downloads
  2. Open and follow the setup wizard
  3. 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:

  1. Find Xeneon Hotkey — In Stream Deck's action list (right sidebar)
  2. Drag Hotkey onto a button
  3. Configure settings — Select the Key (e.g., V) and check Modifiers (e.g., Command)
  4. 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:

  1. Open the Shortcuts app — Cmd+Space -> "Shortcuts"
  2. Create a new shortcut — Named "Paste to iTerm"
  3. Add a Run Shell Script action — With: osascript /path/to/framework/tools/scripts/paste-images-to-iterm.applescript
  4. In Stream Deck — Drag the Shortcuts > Launch Shortcut action onto a button
  5. Set the Shortcut dropdown — To "Paste to iTerm"
  6. 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 via osascript bypasses this limitation.

Source: tools/scripts/paste-images-to-iterm.applescript — uses the AppKit NSPasteboard API 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
  • --sketch flag — 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).

  1. Get an API key — From Google AI Studio
  2. Add it to ~/.claude/settings.json — Under env:
{
  "env": {
    "GEMINI_API_KEY": "your-key-here"
  }
}
  1. 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:

  1. Install the Renovate GitHub App on the framework repo
  2. The renovate.json config is already committed — Renovate reads it automatically
  3. 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:

  1. Go to repo Settings > Code security and analysis
  2. Enable Dependabot alerts
  3. Optionally enable Dependabot security updates for auto-PRs on critical CVEs
  4. 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:

  1. Create account at newreleases.io
  2. Add the 6 repos listed above
  3. Set digest frequency to weekly
  4. 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 --version returns a version
  • gh auth status shows authenticated
  • op account list shows your 1Password account
  • iTerm2 opens and tmux new -s test starts a session
  • Dynamic profiles generated: bash ~/Sites/framework/tools/scripts/generate-iterm-profiles.sh --dry-run lists at least one project
  • claude-projects.json is 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.sh is executable)
  • Cursor Settings Sync is enabled and signed in
  • Claude Code is installed (claude --version) and .mcp.json is configured
  • pandoc --version and typst --version return versions
  • Wispr Flow activates instantly with external mic (no clipping)
  • tailscale status shows your Mac connected (key expiry disabled — no browser tab on reboot)
  • mosh --version returns 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.log shows "Driver active!"
  • Stream Deck software running and buttons respond to touch
  • Stream Deck image paste button sends image to Claude Code in tmux
  • d2 --version returns a version
  • GEMINI_API_KEY is set ([ -n "$GEMINI_API_KEY" ] && echo set)
  • which fff-mcp returns ~/.local/bin/fff-mcp and cat ~/.local/share/fff-mcp/version.txt shows a release tag
  • linear-api prints usage help (linear-api with no args)
  • launchd-ui launches and shows agents from ~/Library/LaunchAgents/ including com.rajababa.fff-mcp-update
  • com.rajababa.fff-mcp-update.plist uses job-wrapper.sh with --healthcheck flag (verify with plutil -p ~/Library/LaunchAgents/com.rajababa.fff-mcp-update.plist)
  • bash ~/Sites/framework/tools/scripts/job-status.sh shows 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.json exists at repo root (verify with plutil -lint renovate.json from 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.plist symlinked: plutil -p ~/Library/LaunchAgents/com.rajababa.shadcn-diff.plist
  • com.rajababa.brew-outdated.plist symlinked: plutil -p ~/Library/LaunchAgents/com.rajababa.brew-outdated.plist
  • bash ~/Sites/framework/tools/scripts/job-status.sh shows shadcn-diff and brew-outdated in the table (after at least one manual run)

Related standards

  • standards/reference/environments.md — Environment strategy and secrets management
  • standards/reference/new-project-checklist.md — Per-project setup after workstation is ready

Last updated: 2026-03-20 (ORC-385: job-wrapper.sh wiring + docs)

Search Framework Explorer

Search agents, skills, and standards