Claude Code on your phone, over Tailscale. Outpost is a background daemon that runs on your Mac and exposes a polished PWA over your tailnet — so you can drive Claude from your phone, approve tool calls as they go, and ship a PR without opening your laptop.
Built for incident response and the in-between moments: get paged, open Outpost, point Claude at the problem, and stay in the loop one tap at a time.
Every project under ~/.claude/projects/ shows up in a grouped list with its live, archived, and worktree-bound sessions. Pick a project, pick a branch (or start a fresh session), and you're in — CLAUDE.md, plugins, and MCP servers inherited from that workspace.
Toggle the session between Ask, Plan, Accept Edits, and Bypass from the header — same vocabulary as the CLI, surfaced as a one-tap menu. Each session remembers its own mode; set a different default for new sessions in Settings.
Every PreToolUse is intercepted. Reads, greps, and other safe calls run automatically per your allowlist; anything else (writes, bash, MCP side effects) queues an inline Approve / Reject card right where it would have run — including inside subagent feeds, so you can stop a runaway agent before it lands.
Each tool call is rendered with per-tool chrome — git-style diffs for Edit, shell-prompt blocks for Bash, ripgrep equivalents for Grep, Read excerpts, and so on — so a transcript scrolls like a story, not a JSON dump.
Agent-spawned work gets its own tabbed feed with the spawn context pinned at the top. Watch each agent's tool calls live, switch between them, and approve their pending calls without losing your place in the parent transcript.
The session's todo list — in progress, completed, with strikethrough — one tap away. Useful for long sessions where you want a glance at "how much is left."
When Claude needs a decision, the question lands as a real card — multi-select where applicable, with a freeform reply box always available.
A full git overlay on every session: browse the log, stage hunks, write a commit message, push, and open a PR — without ever touching a terminal. Powered by local git and gh, so commits and PRs land under your real identity.
A live readout of model, context %, cache breakdown, and your 5-hour / 7-day usage windows — so you know when you're about to hit a limit before Claude does.
Nine hand-tuned palettes — LiveKit, Almanac, Terminal, Nordic, Ink, Botanical, Plasma, Atlas, Library — light and dark, plus the per-default permission mode picker, all in one Settings sheet.
- macOS (this is a launchd LaunchAgent; nothing else is supported).
- Node.js 22+ on
PATH. - Claude Code CLI (
claude) installed and authenticated for the user the daemon runs as. - GitHub CLI (
gh), if you want to use the source-control overlay's "Open PR" flow.brew install gh && gh auth login.
The laptop has to stay awake. The daemon runs locally on your Mac, so if the machine sleeps, the PWA can't reach it. The installer wraps the daemon in
caffeinate -isto block idle and AC-power system sleep, but closing the lid on a Mac without an external display still triggers sleep regardless. In practice that means: leave it plugged in with the lid open while you want to be reachable from your phone.
Tailscale is the encrypted private network that connects your phone to your laptop. It needs to be installed and signed in on both devices under the same Tailscale account — that's what makes them mutually reachable without exposing anything to the public internet.
1a. On the laptop (the one that will run the daemon):
brew install --cask tailscale-app # or download from https://tailscale.com/download/macOpen the Tailscale menu-bar app and sign in. After signing in, verify it's running:
tailscale statusThe first line should show your Mac with a 100.x.y.z IP. If not, click the menu-bar icon → "Connect."
1b. On the phone:
Install the Tailscale app (iOS / Android) and sign in with the same account you used on the laptop. Once signed in, the phone shows up in your tailnet and the two devices can talk to each other.
1c. Enable HTTPS + MagicDNS for the tailnet:
This is a one-time setting in the Tailscale admin console (not on the device). It gives your Mac a stable <host>.ts.net DNS name and lets you provision a real TLS cert for it — both of which the daemon needs.
Follow Tailscale's HTTPS / MagicDNS setup guide. You'll be enabling two features in the admin console: MagicDNS and HTTPS Certificates.
1d. Get your laptop's tailnet hostname (run this on the laptop):
tailscale status --json | jq -r '.Self.DNSName' | sed 's/\.$//'
# → e.g. davids-macbook-pro.tail1234.ts.netSave that hostname — you'll use it in the next step, and again at the end when you open the PWA from your phone.
mkdir -p ~/.outpost
HOST=davids-macbook-pro.tail1234.ts.net # ← your hostname from step 1
tailscale cert \
--cert-file ~/.outpost/$HOST.crt \
--key-file ~/.outpost/$HOST.key \
$HOSTThe daemon reads these files at startup. If they're missing or unreadable it'll exit with the exact tailscale cert command to run, so you can also skip this step and let the daemon tell you what to type.
git clone https://github.com/frostbyte73/outpost.git
cd outpost
npm installThe daemon auto-discovers every project under ~/.claude/projects/ at startup, so there's no "pick one workspace" step — you'll get a project picker in the PWA when starting a new session. Each session inherits the CLAUDE.md, plugins, and MCP servers of its own project.
macOS launchd strips your shell's env when it spawns the daemon, so anything Claude subprocesses need at runtime — GITHUB_TOKEN, MCP server credentials, etc. — has to be made available to the daemon explicitly. The simplest way is a ~/.outpost/.env file the daemon sources at startup:
cat > ~/.outpost/.env <<'EOF'
# Required if you want PR creation / `gh pr view` in the source-control overlay
# to act under a specific identity. Otherwise gh falls back to its own auth.
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Any other secrets your MCP servers or hooks expect, e.g.:
# ANTHROPIC_API_KEY=sk-ant-...
# LINEAR_API_KEY=lin_api_...
EOF
chmod 600 ~/.outpost/.envStandard KEY=value syntax, # comments allowed, export KEY=value tolerated. Anything already set in the plist or shell wins over the file, so the file is the safe place for secrets you don't want baked into LaunchAgent XML.
config/allowlist.default.json ships the defaults for which tools auto-approve vs. queue for explicit confirmation in the PWA. On first daemon start it's copied to config/allowlist.json (gitignored), which is the runtime file the daemon reads and writes. The defaults are tuned for read-only incident-response work — open either file and trim or extend it before going live. See Allowlist below.
install/install.shThis writes ~/Library/LaunchAgents/local.outpost.$USER.plist, loads it, and prints the pid on success. The daemon starts at every login and auto-restarts on crash. Logs land in ~/Library/Logs/outpost.{log,err.log}.
On your phone, make sure the Tailscale app is signed in and toggled on (Tailscale only routes traffic while it's actively connected). Then open this URL in the phone's browser:
https://<your-tailnet-hostname>.ts.net:8443/
…using the hostname you saved in step 1d.
iPhone: open the URL in Safari (not Chrome — the PWA install path only works in Safari on iOS). Tap the Share button, then "Add to Home Screen", then open Outpost from the new icon. This is required for push notifications on iOS — iOS only allows Web Push from PWAs installed to the Home Screen.
On Android Chrome, Web Push works without installing — the Settings page's "Enable push notifications" toggle is all you need.
If the page doesn't load, the usual culprit is Tailscale being toggled off on the phone — open the app and check that it's connected.
The daemon reads env vars at startup, in this precedence: plist EnvironmentVariables > ~/.outpost/.env > built-in defaults. For most users, the defaults are fine and the only thing to touch is ~/.outpost/.env for secrets (see step 4 above).
If you need to override a default permanently, edit ~/Library/LaunchAgents/$PLIST_LABEL.plist and add the key under <key>EnvironmentVariables</key>, then launchctl kickstart -k gui/$UID/$PLIST_LABEL.
| Var | Default | Purpose |
|---|---|---|
OUTPOST_PLIST_LABEL |
local.outpost.$USER |
LaunchAgent label. Export this in your shell before running install/install.sh if you want an org-style prefix like com.example.outpost. |
OUTPOST_HTTPS_PORT |
8443 |
Port the PWA + WebSocket listen on. Change if :8443 is taken. |
OUTPOST_HOOK_PORT |
8444 |
Loopback-only port the PreToolUse hook posts to. Change if :8444 is taken. |
OUTPOST_RUNTIME_DIR |
~/.outpost |
Where certs, allowlist, push subscriptions, and .env live. |
OUTPOST_PROJECTS_ROOT |
~/.claude/projects |
Where the daemon scans for projects/sessions. |
OUTPOST_HOST |
auto-detected | Tailnet hostname used to locate the TLS cert+key in OUTPOST_RUNTIME_DIR. Override if auto-detection picks the wrong one. |
OUTPOST_APPROVAL_TIMEOUT_MS |
600000 (10 min) |
How long a pending approval card waits before the hook auto-rejects. |
Anything else (OUTPOST_CERT_PATH, OUTPOST_KEY_PATH, OUTPOST_ALLOWLIST_PATH, OUTPOST_VAPID_PATH, etc.) exists for power users and edge cases — see src/config.ts for the full list.
config/allowlist.json (created on first daemon start from the tracked config/allowlist.default.json) controls which tool calls run without prompting. There are three lists:
alwaysAllow: exact tool names that always pass (e.g.Read,Grep).alwaysAllowBashPatterns: regex matched against thecommandarg ofBashcalls.alwaysAllowMcpPatterns: regex matched against MCP tool names (mcp__<server>__<tool>).
The defaults are tuned for incident response (kubectl read-only, gh read-only, incident.io read tools, Datadog read tools, etc.). Review and edit before using. Anything not matched gets queued for explicit approval in the PWA.
launchctl unload ~/Library/LaunchAgents/local.outpost.$USER.plist
rm ~/Library/LaunchAgents/local.outpost.$USER.plist
rm -rf ~/.outpostIf you previously installed outpost with the OUTPOST_CWD env var pinning the daemon to one project, no migration is needed beyond removing that line: outpost now discovers every project under ~/.claude/projects/ and asks where to launch each new session via a picker sheet.
Edit ~/Library/LaunchAgents/local.outpost.$USER.plist and delete the <key>OUTPOST_CWD</key> element plus its following <string>...</string>. Then reload the daemon:
launchctl unload ~/Library/LaunchAgents/local.outpost.$USER.plist
launchctl load ~/Library/LaunchAgents/local.outpost.$USER.plistExisting sessions are not migrated — they're already in their per-project dirs and will appear in the new grouped list on first load.
npm run dev # tsx watch, reloads on change
npm start # one-shot daemon
npm test # vitest + playwright
npm run test:unit # vitest only
npm run test:e2e # playwright onlyThe daemon expects to bind :8443 (PWA + WS) and :8444 (loopback hook endpoint). The hook endpoint is loopback-only and authenticated with a per-launch secret that's written into Claude's settings.json at startup — see src/hook-server.ts for the rationale.
Two daemons can't share ~/.outpost/ — index.json files use atomic rename for persistence and a second writer will race. When testing from a checkout (worktree or otherwise) while the prod LaunchAgent is running, stop the LaunchAgent first, then spin up an alternate-port instance:
launchctl bootout gui/$UID/local.outpost.$USER # stop the installed daemon
OUTPOST_HTTPS_PORT=8543 OUTPOST_HOOK_PORT=8544 \
npx tsx src/daemon.ts # the test instanceOpen https://<host>.ts.net:8543/ to drive it. When you're done, restart the real daemon with:
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/local.outpost.$USER.plist(After a working-tree merge, launchctl kickstart -k gui/$UID/local.outpost.$USER is the cleanest way to pick up the new code — unload/load trips on the KeepAlive race.)
The allowlist is the exception to "don't run side-by-side": each checkout has its own gitignored config/allowlist.json seeded from config/allowlist.default.json, so rules hot-added in a worktree don't leak into prod (and vice versa).
src/daemon.ts— wires everything together; main entrypoint.src/server.ts— the HTTPS + WS server that backs the PWA.src/hook-server.ts— the loopback HTTP endpoint Claude'sPreToolUsehook posts to.src/session-manager.ts— owns the live Claude subprocesses and per-session WebSocket fanout.src/session-store.ts— reads session JSONLs off disk for transcript replay.src/worktree-manager.ts— per-session git worktrees under~/.outpost/worktrees/<sessionId>/.src/git-ops.ts— git plumbing for the PWA's source-control overlay (status, log, diff, commit, push, pull,gh prlookups).src/approvals.ts— the pending-approval queue.src/allowlist.ts— matches incoming tool calls against the allowlist.src/pwa/— the static PWA assets served at/.











