Skip to content

frostbyte73/outpost

Repository files navigation

Outpost

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.

Features

Multi-project workspace

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.

Projects list with worktree branches and recent sessions

Permission modes that match the moment

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.

Per-session permission mode picker explaining Ask, Plan, Accept Edits, and Bypass

Selective approvals — no rubber-stamping

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.

Inline approve/reject card inside a subagent feed

Tool tiles that read like the CLI

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.

Edit tile rendered as a unified diff, Bash tile as a shell block

Subagents in their own lanes

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.

Subagent feed showing live commands and the spawn context

Task list panel

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

Task list panel showing 19 of 20 complete

Inline AskUserQuestion

When Claude needs a decision, the question lands as a real card — multi-select where applicable, with a freeform reply box always available.

AskUserQuestion card with options and a freeform reply box

Source control, from your phone

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.

Git log view with recent commits and sync status Diff viewer with stage checkboxes and commit composer Open pull request modal with title and body fields

Context and usage at a glance

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.

Context details panel showing cache breakdown and rate-limit windows

Themes that don't look like every other dev tool

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.

Theme picker and default-mode selector

Prerequisites

  • 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 -is to 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.

Install

1. Set up Tailscale

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/mac

Open the Tailscale menu-bar app and sign in. After signing in, verify it's running:

tailscale status

The 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.net

Save that hostname — you'll use it in the next step, and again at the end when you open the PWA from your phone.

2. Provision a TLS cert+key for that hostname

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 \
  $HOST

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

3. Clone and install deps

git clone https://github.com/frostbyte73/outpost.git
cd outpost
npm install

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

4. (Optional) Set up secrets in ~/.outpost/.env

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/.env

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

5. Review the allowlist

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.

6. Install the LaunchAgent

install/install.sh

This 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}.

7. Open the PWA

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.

Configuration

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.

Allowlist

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 the command arg of Bash calls.
  • 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.

Uninstall

launchctl unload ~/Library/LaunchAgents/local.outpost.$USER.plist
rm ~/Library/LaunchAgents/local.outpost.$USER.plist
rm -rf ~/.outpost

Upgrading from single-cwd outpost

If 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.plist

Existing sessions are not migrated — they're already in their per-project dirs and will appear in the new grouped list on first load.

Development

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 only

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

Running side-by-side with the installed daemon

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 instance

Open 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).

Architecture

  • 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's PreToolUse hook 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 pr lookups).
  • 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 /.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors