Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 60 additions & 41 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

A macOS bootstrap that turns one Mac into a always-reachable host for
running named AI agent sessions — [GitHub Copilot
CLI](https://github.com/github/copilot-cli) and [Claude Code
CLI](https://docs.claude.com/en/docs/claude-code) are both first-class
CLI](https://github.com/github/copilot-cli), [Claude Code
CLI](https://docs.claude.com/en/docs/claude-code), and [Codex
CLI](https://developers.openai.com/codex/cli/) are all first-class
backends, chosen per-command (`ca alpha` for Copilot, `cc alpha` for
Claude) — and reach them from any device (phone, tablet, another
laptop) over [Tailscale SSH](https://tailscale.com/kb/1193/tailscale-ssh)
and `tmux`, with [Termius](https://termius.com) as the terminal
client.
Claude, `co alpha` for Codex) — and reach them from any device (phone,
tablet, another laptop) over [Tailscale
SSH](https://tailscale.com/kb/1193/tailscale-ssh) and `tmux`, with
[Termius](https://termius.com) as the terminal client.

The end state: tap a host in Termius on your phone, land in the same
agent CLI conversation your laptop left mid-thought.
Expand Down Expand Up @@ -36,19 +37,21 @@ section you need from the table of contents.

## What you get

- A single multi-call wrapper (`bin/agent`) installed under four names
- A single multi-call wrapper (`bin/agent`) installed under six names
— `ca` / `copilot-agent` for the Copilot backend, `cc` /
`claude-agent` for the Claude Code backend — that launches or
reattaches to a named agent session. Keychain unlock, `tmux` session
management, and best-effort resumption of the previous CLI session
for that name (Copilot: stable UUID via `--session-id`; Claude:
`--continue` when the workspace has an existing session) are all
handled.
`claude-agent` for the Claude Code backend, `co` / `codex-agent` for
the Codex backend — that launches or reattaches to a named agent
session. Keychain unlock, `tmux` session management, and best-effort
resumption of the previous CLI session for that name (Copilot: stable
UUID via `--session-id`; Claude: `--continue` when the workspace has
an existing session; Codex: `resume --last` when the workspace has an
existing session) are all handled.

Each backend has its own workspace root and its own tmux session
namespace, so the two never collide:
namespace, so the three never collide:
- `ca alpha` → workspace `$COPILOT_WORKSPACE_BASE/agent-alpha`, tmux `alpha`
- `cc alpha` → workspace `$CLAUDE_WORKSPACE_BASE/agent-alpha`, tmux `claude-alpha`
- `co alpha` → workspace `$CODEX_WORKSPACE_BASE/agent-alpha`, tmux `codex-alpha`
- A one-shot installer for OS-level prerequisites: Homebrew, `tmux`,
Tailscale (CLI build), and the MagicDNS resolver fix that Homebrew's
Tailscale formula leaves out.
Expand Down Expand Up @@ -143,9 +146,9 @@ The installer is idempotent — safe to re-run. It:

- Installs Homebrew (if missing), `tmux`, and the Tailscale CLI build.
- Writes `/etc/resolver/ts.net` (the MagicDNS resolver fix).
- Symlinks four names into `/usr/local/bin` — `copilot-agent`,
`ca`, `claude-agent`, `cc` — all pointing at the same multi-call
wrapper (`bin/agent`).
- Symlinks six names into `/usr/local/bin` — `copilot-agent`,
`ca`, `claude-agent`, `cc`, `codex-agent`, `co` — all pointing at
the same multi-call wrapper (`bin/agent`).
- Stamps a managed block into `~/.tmux.conf` that hides tmux's status
bar (the agents are full-screen TUIs and your Termius tabs already
label each session). Toggle it back per-session with `prefix + b`.
Expand Down Expand Up @@ -260,22 +263,29 @@ Install whichever backend CLI(s) you plan to use:
The first launch opens an in-terminal login flow — follow the
prompts to authenticate; the token lands under `~/.claude/`.

You can install one, the other, or both. The wrapper enforces the
- **Codex CLI** — per [the Codex CLI
install](https://developers.openai.com/codex/cli/) (`brew install
--cask codex` or the tarball). Run `codex login` and follow the
ChatGPT auth flow; state lands under `~/.codex/`.

You can install any subset of the three. The wrapper enforces the
presence of the backend binary in PATH only at launch time.

### 1.8 — Workspace locations

Each backend has its own root directory; inside each, agents are
`agent-<name>/` subdirs (e.g., `agent-alpha/`, `agent-bravo/`). The
installer asks for both roots and auto-picks smart defaults:
installer asks for all three roots and auto-picks smart defaults:

- **With Dropbox installed** (recommended — workspaces sync across
Macs):
- Copilot: `~/Library/CloudStorage/Dropbox/copilot-workspace`
- Claude: `~/Library/CloudStorage/Dropbox/claude-workspace`
- Codex: `~/Library/CloudStorage/Dropbox/codex-workspace`
- **Without Dropbox**:
- Copilot: `~/copilot-workspace`
- Claude: `~/claude-workspace`
- Codex: `~/codex-workspace`

Press Enter to accept a default, or type any path (`~`, `$HOME`, and
shell expansions all work). Re-running `install.sh` later keeps
Expand All @@ -286,13 +296,14 @@ To pick paths non-interactively (or from a script):
```bash
./install.sh \
--copilot-workspace-base ~/code/copilot-workspace \
--claude-workspace-base ~/code/claude-workspace
--claude-workspace-base ~/code/claude-workspace \
--codex-workspace-base ~/code/codex-workspace
```

This writes `COPILOT_WORKSPACE_BASE` and `CLAUDE_WORKSPACE_BASE` into
`~/.config/remote-agent-stack/config`. Edit that file directly any
time to move a workspace base later (you'll have to move existing
`<name>/` directories under it by hand).
This writes `COPILOT_WORKSPACE_BASE`, `CLAUDE_WORKSPACE_BASE`, and
`CODEX_WORKSPACE_BASE` into `~/.config/remote-agent-stack/config`. Edit
that file directly any time to move a workspace base later (you'll
have to move existing `<name>/` directories under it by hand).

### 1.9 — Test the wrapper locally

Expand All @@ -308,6 +319,12 @@ Claude Code backend:
cc alpha # tmux session `claude-alpha`, workspace `$CLAUDE_WORKSPACE_BASE/agent-alpha/`
```

Codex backend:

```bash
co alpha # tmux session `codex-alpha`, workspace `$CODEX_WORKSPACE_BASE/agent-alpha/`
```

You should land inside the matching `tmux` session with the backend
CLI running. Detach with `Ctrl-b d` (you're back at the Mac shell).
Reattach with the same command again — should be instant. Now the Mac
Expand Down Expand Up @@ -574,21 +591,24 @@ file looks roughly like:
```sh
COPILOT_WORKSPACE_BASE="/Users/you/.../Dropbox/copilot-workspace" # set during install
CLAUDE_WORKSPACE_BASE="/Users/you/.../Dropbox/claude-workspace" # set during install
CODEX_WORKSPACE_BASE="/Users/you/.../Dropbox/codex-workspace" # set during install
MAILBOX_INTEGRATION="false" # off by default
ALLOW_ALL="false" # off by default
# COPILOT_BIN="copilot"
# CLAUDE_BIN="claude"
# CODEX_BIN="codex"
# AGENT_DIR_PREFIX="agent-"
```

`COPILOT_WORKSPACE_BASE` and `CLAUDE_WORKSPACE_BASE` are whatever you
picked at install time (Dropbox defaults if Dropbox is installed,
otherwise `$HOME/copilot-workspace` and `$HOME/claude-workspace`).
Inside each root, agents live in `agent-<name>/` subdirs (change the
prefix with `AGENT_DIR_PREFIX` if you want a different convention).
Re-run `./install.sh` with `--copilot-workspace-base PATH` /
`--claude-workspace-base PATH` or edit the file directly to move a
root.
`COPILOT_WORKSPACE_BASE`, `CLAUDE_WORKSPACE_BASE`, and
`CODEX_WORKSPACE_BASE` are whatever you picked at install time (Dropbox
defaults if Dropbox is installed, otherwise `$HOME/copilot-workspace`,
`$HOME/claude-workspace`, and `$HOME/codex-workspace`). Inside each
root, agents live in `agent-<name>/` subdirs (change the prefix with
`AGENT_DIR_PREFIX` if you want a different convention). Re-run
`./install.sh` with `--copilot-workspace-base PATH` /
`--claude-workspace-base PATH` / `--codex-workspace-base PATH` or edit
the file directly to move a root.

`MAILBOX_INTEGRATION` enables the cross-session
[mailbox](https://github.com/dfrysinger/skills) skill — `ca` pokes the
Expand All @@ -597,14 +617,13 @@ unless you have the skill installed.

`ALLOW_ALL` makes the wrapper skip permission prompts on new-session
launch — Copilot: `--allow-all` (auto-approves tools, paths, URLs);
Claude Code: `--dangerously-skip-permissions`. Personal-machine
Claude Code: `--dangerously-skip-permissions`; Codex:
`--dangerously-bypass-approvals-and-sandbox`. Personal-machine
convenience; do not enable in shared environments.

`COPILOT_BIN` / `CLAUDE_BIN` let you point the wrapper at a specific
backend binary (e.g., `/opt/homebrew/bin/copilot` or a nightly build).

Additional agent backends (Codex CLI, etc.) are on the roadmap — see
[Status](#status--roadmap).
`COPILOT_BIN` / `CLAUDE_BIN` / `CODEX_BIN` let you point the wrapper
at a specific backend binary (e.g., `/opt/homebrew/bin/copilot` or a
nightly build).

### tmux keychain bootstrap LaunchAgent

Expand Down Expand Up @@ -665,9 +684,9 @@ place — see the uninstaller output for the manual cleanup commands.
## Status & roadmap

Single-machine, single-user, macOS arm64. Tested on macOS Tahoe.
Copilot CLI and Claude Code CLI are both first-class backends today.
Linux support and a Codex CLI backend are [open
issues](https://github.com/dfrysinger/remote-agent-stack/issues) —
Copilot CLI, Claude Code CLI, and Codex CLI are all first-class
backends today. Linux support is an [open
issue](https://github.com/dfrysinger/remote-agent-stack/issues) —
patches welcome.

## Other docs
Expand Down
72 changes: 64 additions & 8 deletions bin/agent
Original file line number Diff line number Diff line change
@@ -1,34 +1,40 @@
#!/usr/bin/env bash
# agent — launch or attach to a named agent session (Copilot or Claude Code)
# agent — launch or attach to a named agent session
# (Copilot CLI, Claude Code CLI, or Codex CLI)
#
# This is a multi-call script: the backend (Copilot CLI vs Claude Code CLI)
# is chosen by the name it's invoked as. install.sh symlinks four names in
# /usr/local/bin:
# This is a multi-call script: the backend is chosen by the name it's
# invoked as. install.sh symlinks six names in /usr/local/bin:
# copilot-agent (or ca) → Copilot CLI backend
# claude-agent (or cc) → Claude Code CLI backend
# codex-agent (or co) → Codex CLI backend
#
# Usage:
# copilot-agent <name> (or: ca <name>)
# claude-agent <name> (or: cc <name>)
# codex-agent <name> (or: co <name>)
#
# Layout: each backend has its own workspace root; inside each, agents
# live in AGENT_DIR_PREFIX-prefixed subdirs so they're easy to spot as
# agent workspaces alongside other project dirs:
# copilot: $COPILOT_WORKSPACE_BASE/agent-<name> (tmux session: <name>)
# claude : $CLAUDE_WORKSPACE_BASE/agent-<name> (tmux session: claude-<name>)
# codex : $CODEX_WORKSPACE_BASE/agent-<name> (tmux session: codex-<name>)
#
# The claude tmux session name is prefixed only so `ca alpha` and `cc alpha`
# can coexist on the same tmux server without collision.
# The claude/codex tmux session names are prefixed only so `ca alpha`,
# `cc alpha`, and `co alpha` can coexist on the same tmux server.
#
# Configuration (env or ~/.config/remote-agent-stack/config):
# COPILOT_WORKSPACE_BASE default: ~/Library/CloudStorage/Dropbox/copilot-workspace
# CLAUDE_WORKSPACE_BASE default: ~/Library/CloudStorage/Dropbox/claude-workspace
# CODEX_WORKSPACE_BASE default: ~/Library/CloudStorage/Dropbox/codex-workspace
# COPILOT_BIN default: copilot
# CLAUDE_BIN default: claude
# CODEX_BIN default: codex
# AGENT_DIR_PREFIX default: agent-
# ALLOW_ALL default: false
# copilot: --allow-all
# claude : --dangerously-skip-permissions
# codex : --dangerously-bypass-approvals-and-sandbox
# MAILBOX_INTEGRATION default: false

set -euo pipefail
Expand All @@ -38,6 +44,7 @@ set -euo pipefail
case "$(basename "$0")" in
ca|copilot-agent) BACKEND="copilot" ;;
cc|claude-agent) BACKEND="claude" ;;
co|codex-agent) BACKEND="codex" ;;
agent) BACKEND="copilot" ;; # fallback for direct invocation
*) BACKEND="copilot" ;;
esac
Expand All @@ -60,8 +67,10 @@ fi

: "${COPILOT_WORKSPACE_BASE:=$HOME/Library/CloudStorage/Dropbox/copilot-workspace}"
: "${CLAUDE_WORKSPACE_BASE:=$HOME/Library/CloudStorage/Dropbox/claude-workspace}"
: "${CODEX_WORKSPACE_BASE:=$HOME/Library/CloudStorage/Dropbox/codex-workspace}"
: "${COPILOT_BIN:=copilot}"
: "${CLAUDE_BIN:=claude}"
: "${CODEX_BIN:=codex}"
: "${AGENT_DIR_PREFIX:=agent-}"

case "$BACKEND" in
Expand All @@ -75,6 +84,11 @@ case "$BACKEND" in
WORKSPACE_BASE="$CLAUDE_WORKSPACE_BASE"
SESSION_PREFIX="claude-"
;;
codex)
AGENT_BIN="$CODEX_BIN"
WORKSPACE_BASE="$CODEX_WORKSPACE_BASE"
SESSION_PREFIX="codex-"
;;
esac

# ---- args ------------------------------------------------------------------
Expand All @@ -93,8 +107,9 @@ Options:

Config:
~/.config/remote-agent-stack/config
Env: COPILOT_WORKSPACE_BASE, CLAUDE_WORKSPACE_BASE, COPILOT_BIN,
CLAUDE_BIN, AGENT_DIR_PREFIX, ALLOW_ALL, MAILBOX_INTEGRATION
Env: COPILOT_WORKSPACE_BASE, CLAUDE_WORKSPACE_BASE, CODEX_WORKSPACE_BASE,
COPILOT_BIN, CLAUDE_BIN, CODEX_BIN, AGENT_DIR_PREFIX, ALLOW_ALL,
MAILBOX_INTEGRATION
USAGE
}

Expand Down Expand Up @@ -279,6 +294,47 @@ case "$BACKEND" in
CMD="$CMD --dangerously-skip-permissions"
fi
;;

codex)
# Codex CLI stores each session as a `rollout-*.jsonl` file under
# ~/.codex/sessions/YYYY/MM/DD/
# The first line is a `session_meta` record whose `cwd` field is the
# working directory Codex was launched from. `codex resume --last` picks
# the most recent session whose cwd matches the current cwd (unless
# --all is passed), so we only need a yes/no answer to "has this agent
# ever run?". If yes → `codex resume --last`; else → fresh `codex`.
#
# Scan strategy: prefer `rg` (fast, common) and fall back to `grep -r`
# so the wrapper still works when ripgrep isn't installed. We match the
# exact `"cwd":"$DIR"` JSON substring against every rollout file, but
# stop at the first hit — session_meta is always on line 1, and the
# search prunes irrelevant files quickly. Local timing: ~350ms for
# ~1000 sessions with rg, well under the human-perceptible threshold.
HAS_SESSION=false
CODEX_SESSIONS_DIR="$HOME/.codex/sessions"
if [ -d "$CODEX_SESSIONS_DIR" ]; then
_cwd_pat="\"cwd\":\"$DIR\""
if command -v rg >/dev/null 2>&1; then
if rg -l --max-count 1 -F "$_cwd_pat" "$CODEX_SESSIONS_DIR" \
>/dev/null 2>&1; then
HAS_SESSION=true
fi
else
if grep -rl --include='*.jsonl' -F "$_cwd_pat" \
"$CODEX_SESSIONS_DIR" 2>/dev/null | head -n1 | grep -q .; then
HAS_SESSION=true
fi
fi
fi

CMD="$AGENT_BIN"
if [ "$HAS_SESSION" = "true" ]; then
CMD="$CMD resume --last"
fi
if [ "${ALLOW_ALL:-false}" = "true" ]; then
CMD="$CMD --dangerously-bypass-approvals-and-sandbox"
fi
;;
esac

[ -n "$MAILBOX_POKE" ] && ( "$MAILBOX_POKE" "$SESSION" --wait ) &
Expand Down
Loading