# Agent Relay Teams: agent-driven signup

You are setting up Agent Relay on behalf of the user who gave you this URL.
Do NOT use computer use, browser automation, screenshots, DOM inspection, or mouse/keyboard control. Fetch the guide over HTTP and perform setup through its documented APIs. Use terminal commands only for HTTP requests and the documented installation/CLI operations. The user owns the screen: open approval URLs with the OS URL opener (or give the user the link), then wait and poll the documented API. Never operate the approval page or the desktop app UI.
Do the API calls, installation, configuration, and verification yourself. Keep
working across user approvals; do not hand the user a checklist to execute.
The signup page is a spectator view for the user. Do not read or control it;
report progress only with GET/PATCH on the supplied Progress API URL.
For an approval URL, use the OS URL opener (for example macOS open with the
URL passed as a separate subprocess argument, never interpolated into shell
code). If no opener is available, give the link to the user. Do not launch a
browser-control tool. Verify approval by polling the API, never by inspecting
the browser. If an operation has no documented API or CLI, report the blocker
and ask the user for that specific action; never fall back to computer use.
For desktop installation, download the prebuilt binary the skill names. Never
clone the desktop repository, install build dependencies, run a build, compile
from source, or generate a DMG. A missing binary is a blocker, not a build task.
The user handles Google sign-in, device approval, and any provider or operating
system consent. Never approve access on their behalf or ask for their password.

Site: https://agentrelay.com
Cloud API base: https://agentrelay.com/cloud
Use this exact environment throughout; never fall back from local development
to production.

Send a User-Agent naming your tool (for example agent-relay-setup/1.0) on every
HTTP request, including the guide and the Cloud APIs. The edge rejects Python
urllib's default (Python-urllib/x.y) with HTTP 403 and a plain-text
"error code: 1010" body before the API sees the request. API errors are JSON
objects with an error code. For any response that is not JSON, report only the
HTTP status, never the body.

## How this guide is built

This header holds only what is specific to signup. The setup steps are the
canonical Agent Relay skills from https://github.com/AgentWorkforce/skills,
included below verbatim as published to the prpm registry:

- Part 1: Cloud account (@agent-relay/signing-in-to-agent-relay-cloud@0.1.0)
- Part 2: Desktop (@agent-relay/setting-up-agent-relay-desktop@1.1.10)
- Part 3: Sessions (@agent-relay/setting-up-agent-relay-sessions@0.2.8)

Follow the parts in order. Where a part says to ask the human, ask the user
in your conversation with them.
Where a part offers to install itself as a skill or to use another agent,
continue with the text here instead. A part that requires another skill finds
it as another part of this guide; do not stop to install skills.

Part 1 creates the account with signup_source "teams" for signup attribution.
Complete account setup before Part 2. Pass Part 1's currentWorkspace.id as the
workspace in Part 2's /setup/sign-in payload. The app signs in on its own, so
the user may approve one more device link there unless it reuses a login.
Before Part 2, read Part 3's "The only human steps" and ask the user for those
inputs up front, so they can mint the token while the desktop installs.
Setup is complete only in Part 3's finished state: a proven live round trip.

## Live progress (when the user's prompt includes a progress session)

The user is watching a setup page. Report real milestones using the Progress API
and Progress token supplied in their prompt. The token authorizes progress only;
it is NOT a Cloud access token. Never send account tokens, OAuth codes, passwords,
logs, approval URLs, or sensitive personal information to the progress endpoint.
 Do not put
the progress token in URLs or output it in your final reply. Use the same site
and /cloud origin shown above; never forward it to another environment.

GET the supplied Progress API URL to obtain the current revision and product.
Check that the product matches this guide. Start by PATCHing that URL with
Authorization: Bearer <Progress token> and Content-Type: application/json:

~~~json
{"step":1,"state":"working","revision":0,"agent":"<your agent type>"}
~~~

Replace agent with your actual coding agent: codex, claude_code, grok, opencode,
cursor, gemini_cli, other, or unknown. Report the tool running this setup, not
its underlying model or the agents the user will run later. Use unknown if you
cannot determine it; never guess from the logos on the page. Include agent in
your first PATCH. It is stored once for signup funnel attribution; later PATCHes
may omit it. A different known agent returns 409 agent_conflict: preserve the
original attribution and omit agent when resuming from a different tool.

Use the revision returned by the latest GET/PATCH, not the example's literal 0.
PATCH before each numbered progress step below. A move to the next step marks
the previous step done; do not skip steps or report success before checking it.
Set state: waiting when you need the user to approve access or choose an option.
Set state: working at the same step when you resume, and state: failed if work
cannot continue. Only report complete after the actual checks succeed.

Progress steps for Teams:
1. Sign in: before Part 1.
2. Install the app: before Part 2's install (its sections 1 and 2).
3. Connect the workspace: before Part 2's sign-in (its section 3).
4. Choose what to share: before Part 2's defaults (its section 4). Report
   waiting while the user decides about an existing sharing mode.
5. Check everything works: before Part 2's final verification, continuing
   through all of Part 3. Keep step 5 working or waiting until Part 3 proves a
   live round trip; only then PATCH step: 5, state: complete. A self-verified
   read path without a teammate is not completion.

On HTTP 409, GET current progress and reconcile; never overwrite newer progress
or regress a step. If a PATCH response is lost, GET before retrying. If a step is
already complete, verify the actual account/app/flow state before continuing;
progress reports alone are not proof that setup succeeded. On 429, honor
Retry-After. Retry transient network/5xx failures with bounded backoff. On 404,
stop reporting (the session expired or the token is invalid) and tell the user;
do not recreate or switch their session silently. A progress service outage
must not roll back working setup or cause duplicate installation/activation.

---

# Part 1: Cloud account

# Sign In to Agent Relay Cloud

Use the existing OAuth device flow. No API key, invitation, dashboard wizard,
or pre-existing Agent Relay account is required. Only the human approves the
sign-in; never approve access on their behalf, ask for their password, or
operate the approval page yourself.

Paths below are relative to the Cloud API base, `https://agentrelay.com/cloud`,
including its `/cloud` prefix. When a guide names a different Cloud base (for
example a local development stack), use that exact base throughout and never
fall back to production.

## HTTP client

Use `curl` or `fetch`. Avoid Python's `urllib` or `requests` with their
default User-Agent: the device-token endpoint answers them with HTTP 403 and no
JSON body (tracked in AgentWorkforce/cloud#4254). Never print a raw auth
response while debugging; on success it contains tokens. Send JSON request
bodies with `Content-Type: application/json`, set a 30-second request timeout,
and check every response status before proceeding.

## 1. Start the device grant

`POST /api/v1/auth/device/start` with:

```json
{"client_name":"My agent setup"}
```

An agent-driven signup guide also passes its product as `signup_source`
(`"teams"` or `"flows"`); keep the value the guide names. Omit it otherwise.

Expect HTTP 201 with `device_code`, `user_code`, `verification_uri_complete`,
`verification_uri`, `interval` (seconds), and `expires_in` (seconds). Keep
`device_code` private. Open `verification_uri_complete` in the person's
browser with the OS URL opener (for example macOS `open`, with the URL passed as
a separate argument, never interpolated into shell code), or give them the link
if no opener is available. Show the `user_code` so they can compare it. The page
lets them sign in with Google, review the requesting device, and Approve or
Deny. A signup marker in the returned URL creates the right account type;
preserve it through sign-in. Do not call `/auth/device/approve` yourself.

For a fresh signup, you can open `/api/auth/google/start?next=<encoded-return-path>`
on the same Cloud base first (in production,
`https://agentrelay.com/cloud/api/auth/google/start?next=<encoded-return-path>`), where `encoded-return-path` is the URL-encoded pathname plus query of
`verification_uri_complete`. This opens Google immediately and returns to the
same device approval with its code and signup marker intact. Never open the
production URL for a non-production base: that would sign the human in to
production instead.

## 2. Poll for tokens

While the browser is open, wait `interval` seconds between POSTs to
`/api/v1/auth/device/token` with this JSON (substitute the private
`device_code`):

```json
{"grant_type":"urn:ietf:params:oauth:grant-type:device_code","device_code":"<device_code>"}
```

- `authorization_pending`: keep waiting; respect a returned interval.
- `slow_down`: increase the interval by at least 5 seconds.
- HTTP 429: respect `Retry-After` and increase the interval.
- HTTP 5xx or request timeout: retry with backoff, bounded by `expires_in`.
- `access_denied`: stop. `expired_token` or `invalid_grant`: explain and start
  a new grant only if the person still wants to continue. Never poll past
  expiry.

HTTP 200 returns `access_token`, `refresh_token`, `access_token_expires_at`,
`refresh_token_expires_at`, `api_url` and `token_type`. Keep credentials in
memory or a private file (directory 0700, file 0600) outside repositories.
Never echo tokens, put them in chat or URLs, or dump full authentication
responses. Use `Authorization: Bearer <access_token>` for subsequent Cloud
requests. Reject an `api_url` pointing at another origin; keep using the Cloud
base above.

## 3. Refresh before expiry

Before expiry, `POST /api/v1/auth/token/refresh` with
`{"refreshToken":"<refresh_token>"}`. The response uses camelCase:
`accessToken`, `refreshToken`, `accessTokenExpiresAt`, `refreshTokenExpiresAt`,
`apiUrl`. Replace both stored tokens atomically. Serialize refreshes: the
refresh token rotates and must not be shared between machines. An
invalid/expired refresh requires a new device login, not an endless retry.

## 4. Read the account and workspace

`GET /api/v1/auth/whoami`. Require `authenticated: true` and read `user.id`,
`user.email`, `currentWorkspace.id`, and `currentOrganization.id`. New signups
create a workspace automatically. Reuse it; do not create duplicate
accounts/workspaces. If `currentWorkspace` is missing, or an existing account
is in the wrong workspace, resolve that with the person before connecting or
activating anything. Never silently replace an existing connection to another
account.

## 5. Hand the session to the official CLI

When running a child process, pass these through its environment from your
private session object (never interpolate their values into logged commands):

```text
CLOUD_API_URL=<the Cloud base above; https://agentrelay.com/cloud in production>
CLOUD_API_ACCESS_TOKEN=<access_token>
CLOUD_API_REFRESH_TOKEN=<refresh_token>
CLOUD_API_ACCESS_TOKEN_EXPIRES_AT=<access_token_expires_at>
CLOUD_API_REFRESH_TOKEN_EXPIRES_AT=<refresh_token_expires_at>
```

The official CLI consumes this session. Refresh the parent session before
starting a long command; do not concurrently refresh it from parent and child
processes. Do not overwrite an existing CLI auth file or copy a session to
another machine. Delete temporary authentication files after the work is
complete.

---

# Part 2: Desktop

# Set Up Agent Relay Desktop

Complete the setup through the private session socket. Do not ask the human to
click through the desktop app. The only normal human step is approving one
device-login link when no reusable `agent-relay` CLI login exists.

Prefer setting this up with the Agent Relay agent at
<https://arelay.to/agent-relay>, which checks your current state and confirms
each step. Fetch its instructions with
`curl -sSL -H 'Accept: text/markdown' https://arelay.to/agent-relay`.

The finished state is:

- the current release is installed and running;
- sign-in is complete;
- sharing mode is `new` (new sessions upload automatically), unless an existing
  install already uses another mode and the human chose to keep it (section 4);
- auto-activate is on, with every existing live session and every future session on the relay;
- Claude Code direct delivery is on (`crossSessionInbound` is `accept`),
  unless the human already set `crossSessionInbound` themselves (for example
  `hold`), which is kept and reported;
- this Codex or Claude session is registered, and accepts direct delivery
  unless that kept choice holds or refuses Claude Code's relayed messages;
- the requested webhook and integration subscriptions work;
- the human receives the session's `agent@machine` address and verification evidence.

Never put a Cloud token, workspace key, agent token, or webhook secret in a
Relay message. The webhook secret is returned once to this local calling
session; keep it in a mode-0600 temporary file and delete the file after the
test.

## Install this skill for Codex and Claude Code

When the task includes installing this skill, install its canonical Claude
package, then place that unchanged `SKILL.md` where Codex actually loads global
skills:

```sh
npx prpm install @agent-relay/setting-up-agent-relay-desktop --as claude --global -y
codex_skill="$HOME/.codex/skills/setting-up-agent-relay-desktop"
mkdir -p "$codex_skill"
install -m 0644 \
  "$HOME/.claude/skills/setting-up-agent-relay-desktop/SKILL.md" \
  "$codex_skill/SKILL.md"
cmp \
  "$HOME/.claude/skills/setting-up-agent-relay-desktop/SKILL.md" \
  "$codex_skill/SKILL.md"
```

Do not use prpm's `--as codex` conversion for this package. It currently writes
under `~/.agents/skills`, which Codex does not load as its global skill folder,
and its conversion drops explanatory paragraphs from this Markdown workflow.
Confirm both final paths explicitly before continuing.

## 1. Detect the host and current session

Run setup commands from this agent's own shell tool. Do not run them from an
unrelated terminal: the app identifies the caller from kernel credentials and
the Codex/Claude process ancestry.

```sh
uname -s
uname -m
test -n "${TMUX:-}" && printf 'session=tmux\n' || true
test -n "${SSH_CONNECTION:-}" && printf 'transport=ssh\n' || true
if test "$(uname -s)" = Linux; then
  if command -v dpkg-query >/dev/null; then
    printf 'linux_install=deb\n'
  else
    printf 'linux_install=per-user-tarball\n'
  fi
fi
command -v systemctl >/dev/null && systemctl --user is-system-running || true
```

On Ubuntu with `dpkg`, prefer the `.deb` path below. On Arch-based and other
systemd Linux hosts without `dpkg`, use the checksum-verified per-user tarball
path; do not try to install the `.deb`. The app scans same-user process trees
and TTYs; it does not require the service itself to share the tmux session.

`systemctl --user is-system-running` may report `degraded` because an unrelated
user unit failed. That is not a Relay failure when
`systemctl --user is-active agent-relay.service` reports `active` and the Relay
socket passes its status check.

Find an existing socket without printing private files:

```sh
pointer="$HOME/.agentworkforce/desktop/relay-socket"
if test -r "$pointer"; then relay_socket=$(sed -n '1p' "$pointer"); fi
if test -n "${relay_socket:-}" && ! test -S "$relay_socket"; then unset relay_socket; fi
if test -z "${relay_socket:-}" && test -n "${XDG_RUNTIME_DIR:-}"; then
  relay_socket="$XDG_RUNTIME_DIR/agent-relay/relay.sock"
fi
if ! test -S "${relay_socket:-/nonexistent}" && test "$(uname -s)" = Linux; then
  relay_socket="/run/user/$(id -u)/agent-relay/relay.sock"
fi
c="$HOME/Library/Application Support/com.agentrelay.desktop/run/relay.sock"
if ! test -S "${relay_socket:-/nonexistent}" && test "$(uname -s)" = Darwin && test -S "$c"; then   # pointer file missing: use the app's own socket
  relay_socket=$c
fi
test -S "${relay_socket:-/nonexistent}" && printf 'socket=%s\n' "$relay_socket"
```

Shell variables do not survive between separate tool calls. Every later block
uses `relay_socket`, and some use variables set earlier in the same section (the
sign-in poll uses `sign_in`, the sign-in request's response). Run a section's
blocks in one shell. In a new shell, re-establish every variable the next block
reads first: re-run this discovery block for `relay_socket`, and never re-send a
sign-in request just to restore `sign_in`; the poll falls back to a 5-second
interval without it.

If the socket exists, inspect it before installing anything:

```sh
# Checks only that a socket file exists; the curl below is what proves the
# app answers (a crashed app can leave a stale socket file behind).
test -S "${relay_socket:-/nonexistent}" || { printf 'relay_socket is unset or names no socket file (%s): run the socket discovery block first.\n' "${relay_socket:-unset}" >&2; exit 1; }
curl -sS --max-time 60 --unix-socket "$relay_socket" http://relay/setup/status | jq
```

**Slow socket replies are known.** `GET /setup/status` and `GET /agents` can
take about 30 seconds on a busy Desktop (tracked in
AgentWorkforce/relay-desktop#333). Give each socket call a 60-second timeout
(`curl --max-time 60`), and do not treat a slow reply as a failure. The probe's
`relay socket-request` can give up sooner with "Probe could not finish. Check
your connection and run setup again"; on these paths that is the same latency,
not a setup problem. Retry the call once before reporting a blocker, and do not
start a second install or sign-in because of it.

Preserve a working newer install. When the socket is absent, check for an
installed-but-stopped copy before downloading: use `dpkg-query -W agent-relay`
when `dpkg-query` exists and `systemctl --user status agent-relay.service` on
Linux, or test `/Applications/Agent Relay.app` on macOS. Start it and retry
status first.
Update an older install only when it lacks the setup contract or reports an
older version. A `404` from `/setup/status` means the installed build is too
old for agent-driven setup and should be updated.

## 2. Install or update with checksum verification

Use a private temporary directory. Never skip the published SHA-256 check.

### Ubuntu LTS, headless `.deb` (preferred)

Select the release architecture and install:

This path needs passwordless sudo or one human-approved sudo prompt for the
package install and `loginctl enable-linger`. Explain that prerequisite before
starting; never capture or relay a sudo password.

```sh
case "$(uname -m)" in
  x86_64|amd64) relay_arch=x64 ;;
  aarch64|arm64) relay_arch=arm64 ;;
  *) printf 'Unsupported Linux architecture: %s\n' "$(uname -m)" >&2; exit 2 ;;
esac
relay_tmp=$(mktemp -d)
chmod 700 "$relay_tmp"
release=https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download
curl -fL "$release/AgentRelay-Linux-$relay_arch.deb" -o "$relay_tmp/AgentRelay.deb"
curl -fL "$release/AgentRelay-Linux-$relay_arch.deb.sha256" -o "$relay_tmp/AgentRelay.deb.sha256"
if ! (cd "$relay_tmp" && sed "s#AgentRelay-Linux-$relay_arch.deb#AgentRelay.deb#" AgentRelay.deb.sha256 | sha256sum --check -); then
  printf 'Agent Relay checksum verification failed; refusing installation.\n' >&2
  exit 1
fi
sudo apt-get install -y "$relay_tmp/AgentRelay.deb"
sudo loginctl enable-linger "$USER"
systemctl --user daemon-reload
systemctl --user enable agent-relay.service
systemctl --user restart agent-relay.service
systemctl --user is-active agent-relay.service
loginctl show-user "$USER" -p Linger
```

The unit runs the bundled core headless: no display, X server, or GTK
window is needed. This session's own GitHub subscriptions (section 6) go
through the Desktop and need no CLI. The separate Agent Relay CLI is needed
only to subscribe a fleet-spawned or named agent; record whether it is present
and at least 13.0.0. Find the same user-level npm, mise, or nvm CLI the app
discovers; never run the desktop's `/usr/bin/agent-relay` launcher as a CLI:

```sh
# The installed Agent Relay CLI: user-level npm, mise or nvm, then PATH;
# never the .deb's /usr/bin/agent-relay Desktop launcher. Section 2 and
# section 6 define this identically; change both together.
find_relay_cli() {
  for candidate in \
    "$HOME/.local/bin/agent-relay" \
    "$HOME/.npm-global/bin/agent-relay" \
    "$HOME/.agentworkforce/relay/bin/agent-relay"
  do
    if test -x "$candidate" && test "$candidate" != /usr/bin/agent-relay; then
      printf '%s\n' "$candidate"; return
    fi
  done
  for relay_root in \
    "$HOME/.local/share/mise/installs/node" \
    "$HOME/.nvm/versions/node"
  do
    test -d "$relay_root" || continue
    candidate=$(find -L "$relay_root" -mindepth 3 -maxdepth 3 \
      -path '*/bin/agent-relay' -type f -perm -u+x -print -quit 2>/dev/null)
    if test -n "$candidate" && test "$candidate" != /usr/bin/agent-relay; then
      printf '%s\n' "$candidate"; return
    fi
  done
  candidate=$(command -v agent-relay 2>/dev/null || true)
  if test -n "$candidate" && test -x "$candidate" && test "$candidate" != /usr/bin/agent-relay; then
    printf '%s\n' "$candidate"
  fi
}
relay_cli=$(find_relay_cli)
if test -n "$relay_cli"; then
  "$relay_cli" --version
  "$relay_cli" integration subscribe --help >/dev/null
else
  printf 'No Agent Relay CLI found; Desktop subscriptions do not need one.\n'
fi
```

On Linux without `dpkg`, use this per-user tarball installation under a
versioned user directory. It is also the fallback on Ubuntu when `.deb`
installation is unavailable and the human approves a per-user install. The
archive contains a unit whose `/usr/bin` path is correct for packages, so
rewrite only that `ExecStart` in the user copy:

```sh
case "$(uname -m)" in
  x86_64|amd64) relay_arch=x64 ;;
  aarch64|arm64) relay_arch=arm64 ;;
  *) printf 'Unsupported Linux architecture: %s\n' "$(uname -m)" >&2; exit 2 ;;
esac
relay_tmp=$(mktemp -d)
chmod 700 "$relay_tmp"
release=https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download
curl -fL "$release/AgentRelay-Linux-$relay_arch.tar.gz" -o "$relay_tmp/AgentRelay.tar.gz"
curl -fL "$release/AgentRelay-Linux-$relay_arch.tar.gz.sha256" -o "$relay_tmp/AgentRelay.tar.gz.sha256"
if ! (cd "$relay_tmp" && sed "s#AgentRelay-Linux-$relay_arch.tar.gz#AgentRelay.tar.gz#" AgentRelay.tar.gz.sha256 | sha256sum --check -); then
  printf 'Agent Relay checksum verification failed; refusing installation.\n' >&2
  exit 1
fi
mkdir "$relay_tmp/extracted"
tar -xzf "$relay_tmp/AgentRelay.tar.gz" -C "$relay_tmp/extracted"
relay_tree="$relay_tmp/extracted/AgentRelay-linux-$relay_arch"
test -x "$relay_tree/usr/bin/agent-relay"
relay_version=$(sed -n 's/^__version__ = "\([0-9A-Za-z._-]*\)"$/\1/p' \
  "$relay_tree/usr/lib/agent-relay/agent_relay/__init__.py")
case "$relay_version" in
  ''|*[!0-9A-Za-z._-]*) printf 'Invalid Agent Relay version in archive.\n' >&2; exit 1 ;;
esac
relay_unit="$HOME/.config/systemd/user/agent-relay.service"
relay_installed_version=
if test -S "${relay_socket:-/nonexistent}"; then
  relay_installed_version=$(curl -fsS --max-time 60 --unix-socket "$relay_socket" \
    http://relay/setup/status 2>/dev/null | jq -r '.data.version // empty' || true)
fi
if test -z "$relay_installed_version" && test -r "$relay_unit"; then
  relay_installed_version=$(sed -n \
    's#^ExecStart=.*/\.local/lib/agent-relay/\([^/ ]*\)/.*#\1#p' \
    "$relay_unit")
fi
case "$relay_installed_version" in
  ''|*[!0-9A-Za-z._-]*) relay_installed_version= ;;
esac
if test -n "$relay_installed_version" && \
   test "$relay_installed_version" != "$relay_version" && \
   test "$(printf '%s\n%s\n' "$relay_version" "$relay_installed_version" | sort -V | tail -n 1)" = "$relay_installed_version"; then
  printf 'Installed Agent Relay %s is newer than archive %s; refusing downgrade.\n' \
    "$relay_installed_version" "$relay_version" >&2
  exit 1
fi
relay_prefix="$HOME/.local/lib/agent-relay/$relay_version"
mkdir -p "$relay_prefix"
cp -a "$relay_tree/usr" "$relay_prefix/"
mkdir -p "$(dirname "$relay_unit")"
sed "s#^ExecStart=/usr/#ExecStart=$relay_prefix/usr/#" \
  "$relay_tree/usr/lib/systemd/user/agent-relay.service" >"$relay_unit.tmp"
chmod 0644 "$relay_unit.tmp"
mv "$relay_unit.tmp" "$relay_unit"
grep -F "ExecStart=$relay_prefix/usr/" "$relay_unit" | grep -q -- ' --headless$'
if ! loginctl show-user "$USER" -p Linger | grep -qx 'Linger=yes'; then
  sudo loginctl enable-linger "$USER"
fi
systemd-analyze --user verify "$relay_unit"
systemctl --user daemon-reload
systemctl --user enable agent-relay.service
systemctl --user restart agent-relay.service
systemctl --user is-active agent-relay.service
loginctl show-user "$USER" -p Linger
# List the app in the desktop's app launcher (no-op on a headless server).
# Releases before v2026.10.13 lack agent-relay-desktop; the app then adds the
# entry the first time it is opened.
if test -x "$relay_prefix/usr/bin/agent-relay-desktop"; then
  "$relay_prefix/usr/bin/agent-relay-desktop" --install-launcher || true
fi
```

The unit's `ExecStart` names the archive's own path under `/usr/`, either the
launcher or the bundled `agent-relay-probe`; the rewrite moves whichever it is
under `$relay_prefix`. The launcher entry runs `agent-relay-desktop` by its
full path, never the bare `agent-relay`, which is also the CLI's name.

Do not improvise Alpine or RHEL distribution packages. On a compatible
systemd host without `dpkg`, use only the verified per-user tarball path above;
non-systemd hosts remain unsupported by this Linux workflow.

### macOS DMG

```sh
case "$(uname -m)" in
  arm64) relay_arch=arm64 ;;
  x86_64) relay_arch=x64 ;;
  *) printf 'Unsupported Mac architecture: %s\n' "$(uname -m)" >&2; exit 2 ;;
esac
relay_tmp=$(mktemp -d)
chmod 700 "$relay_tmp"
release=https://github.com/AgentWorkforce/relay-desktop-releases/releases/latest/download
curl -fL "$release/AgentRelay-macOS-$relay_arch.dmg" -o "$relay_tmp/AgentRelay.dmg"
curl -fL "$release/AgentRelay-macOS-$relay_arch.dmg.sha256" -o "$relay_tmp/AgentRelay.dmg.sha256"
if ! (cd "$relay_tmp" && sed "s#AgentRelay-macOS-$relay_arch.dmg#AgentRelay.dmg#" AgentRelay.dmg.sha256 | shasum -a 256 --check -); then
  printf 'Agent Relay checksum verification failed; refusing installation.\n' >&2
  exit 1
fi
mount_point=$(hdiutil attach -nobrowse -readonly "$relay_tmp/AgentRelay.dmg" | awk '/\/Volumes\// {sub(/^.*\/Volumes\//,"/Volumes/"); print; exit}')
incoming_id=$(defaults read "$mount_point/Agent Relay.app/Contents/Info" CFBundleIdentifier 2>/dev/null || true)
if test "$incoming_id" != com.agentrelay.desktop; then
  hdiutil detach "$mount_point" >/dev/null 2>&1 || true
  printf 'Downloaded app has unexpected bundle id %s; refusing installation.\n' "$incoming_id" >&2
  exit 1
fi
if test -d "/Applications/Agent Relay.app"; then
  existing_id=$(defaults read "/Applications/Agent Relay.app/Contents/Info" CFBundleIdentifier 2>/dev/null || true)
  if test "$existing_id" != com.agentrelay.desktop; then
    hdiutil detach "$mount_point" >/dev/null 2>&1 || true
    printf 'Existing Agent Relay.app has unexpected bundle id %s; refusing overwrite.\n' "$existing_id" >&2
    exit 1
  fi
fi
osascript -e 'tell application "Agent Relay" to quit' 2>/dev/null || true
ditto "$mount_point/Agent Relay.app" "/Applications/Agent Relay.app"
hdiutil detach "$mount_point"
open -a "Agent Relay"
```

If `/Applications` needs administrator access, use `sudo ditto` only after the
human has authorized installation. Do not remove or overwrite another app with
an unexpected bundle identity.

After either install, wait up to 30 seconds for the pointer:

```sh
app_sock="$HOME/Library/Application Support/com.agentrelay.desktop/run/relay.sock"
for _ in $(seq 1 30); do
  test -r "$HOME/.agentworkforce/desktop/relay-socket" && break
  test "$(uname -s)" = Darwin && test -S "$app_sock" && break   # the app is already listening
  sleep 1
done
relay_socket=$(sed -n '1p' "$HOME/.agentworkforce/desktop/relay-socket" 2>/dev/null)
if ! test -S "$relay_socket" && test "$(uname -s)" = Darwin && test -S "$app_sock"; then
  relay_socket=$app_sock
fi
if ! test -S "$relay_socket" && test "$(uname -s)" = Linux; then
  relay_socket="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/agent-relay/relay.sock"
fi
test -S "$relay_socket"
curl -fsS --max-time 60 --unix-socket "$relay_socket" http://relay/setup/status | jq -e '.ok and (.data.version | length > 0)'
```

## 3. Sign in by device approval

Before choosing a workspace, check what the human already has. These commands
only read state. The CLI checks run only when section 2 found a `relay_cli`;
never call the `.deb`'s `/usr/bin/agent-relay` launcher for them:

```sh
curl -sS --max-time 60 --unix-socket "$relay_socket" http://relay/setup/status | \
  jq '{sign_in: .data.sign_in, workspace: .data.workspace}'
if test -n "${relay_cli:-}"; then
  "$relay_cli" cloud whoami       # signed in, and as whom
  "$relay_cli" workspace active   # the active Cloud workspace (keys stay masked)
  "$relay_cli" cloud workspaces   # every workspace this login can use, with ids
  "$relay_cli" status             # workspace, cloud login and local broker
fi
```

Sign in to the workspace they already use. Pick a different or new workspace
only when the human explicitly asks for it.

For a multi-server rollout, obtain the existing enterprise workspace UUID from
the human and pass it explicitly on every server. Do not guess it and do not
use `{}` when the target workspace is known: that could select a default/free
workspace and split the fleet. Use `{}` only for an individual setup where the
human has confirmed that the reusable login's current workspace is intended.

```sh
relay_workspace_uuid='HUMAN-PROVIDED-WORKSPACE-UUID'
sign_in_payload=$(jq -nc --arg workspace "$relay_workspace_uuid" '{workspace:$workspace}')
sign_in=$(curl -sS --max-time 60 --unix-socket "$relay_socket" \
  -H 'Content-Type: application/json' \
  -d "$sign_in_payload" http://relay/setup/sign-in)
printf '%s\n' "$sign_in" | jq \
  '{ok, data: {
    status: .data.status,
    reused_login: .data.reused_login,
    workspace: .data.workspace
  }, error}'
```

If sign-in reports that older upload schedules could not be inspected, first
inspect the user's legacy cron, launchd, and user-service upload schedules.
After completing that safety check, retry the original request with the
acknowledgement added; never use it merely to suppress the error:

```sh
sign_in_payload=$(printf '%s\n' "$sign_in_payload" | \
  jq '. + {acknowledge_uninspected_schedules:true}')
sign_in=$(curl -sS --max-time 60 --unix-socket "$relay_socket" \
  -H 'Content-Type: application/json' \
  -d "$sign_in_payload" http://relay/setup/sign-in)
printf '%s\n' "$sign_in" | jq \
  '{ok, data: {
    status: .data.status,
    reused_login: .data.reused_login,
    workspace: .data.workspace
  }, error}'
```

Print a device link and code only when the response actually contains a code:

```sh
user_code=$(printf '%s\n' "$sign_in" | jq -r '.data.user_code // empty')
if test -n "$user_code"; then
  verification_url_complete=$(printf '%s\n' "$sign_in" | \
    jq -r '.data.verification_url_complete // empty')
  if test -n "$verification_url_complete"; then
    printf 'Open %s and approve code %s\n' "$verification_url_complete" "$user_code"
  else
    verification_url=$(printf '%s\n' "$sign_in" | \
      jq -r '.data.verification_url // empty')
    printf 'Open %s and enter code %s\n' "$verification_url" "$user_code"
  fi
fi
```

If the response is `signed_in` with `reused_login:true`, the app reused an
eligible same-origin CLI or desktop login and intentionally issued no device
code. Confirm the returned workspace id and name with the human rather than
waiting for a code, then continue only if it is the intended workspace.

For `pending_approval`, the human can approve from any phone or browser. Do not
create another code while this one is pending. A `preparing` response is also
normal: poll `/setup/status` at the reported interval. Preparing local history
can take about a minute when the app reuses an existing `agent-relay` CLI login.
Poll both states until sign-in finishes. The reason for a failure is in
`.data.sign_in_message`, not in `.data.sign_in` (which only says `error`), so
print it, and map the known messages to their fix instead of dumping raw state:

```sh
while :; do
  state=$(curl -sS --max-time 60 --unix-socket "$relay_socket" http://relay/setup/status)
  phase=$(printf '%s' "$state" | jq -r '.data.sign_in // .error.code')
  case "$phase" in
    signed_in)
      printf '%s\n' "$state" | jq \
        '{sign_in: .data.sign_in, workspace: .data.workspace}'
      break
      ;;
    preparing|pending_approval) relay_unknown=0 ;;
    signed_out|not_signed_in)
      # signed_out: the attempt ended or was cancelled; not_signed_in: the error code.
      printf 'Sign-in is not in progress; send the sign-in request again (see Recovery).\n' >&2
      exit 1
      ;;
    denied|expired|error)
      msg=$(printf '%s' "$state" | jq -r '.data.sign_in_message // .error.message // "no message reported"')
      printf 'Sign-in %s: %s\n' "$phase" "$msg" >&2
      case "$msg" in
        *"older managed history uploader"*)
          printf 'See "Sign-in blocked by a legacy uploader" under Recovery.\n' >&2 ;;
        *"older upload schedules"*)
          printf 'Inspect legacy schedules, then retry with acknowledge_uninspected_schedules (see above).\n' >&2 ;;
        *) printf '%s\n' "$state" | jq '.data' >&2 ;;
      esac
      exit 1
      ;;
    *)
      # A failed or empty status read: allow three in a row, then stop and report.
      relay_unknown=$(( ${relay_unknown:-0} + 1 ))
      printf 'Unexpected sign-in state: %s (attempt %s of 3)\n' "${phase:-none}" "$relay_unknown" >&2
      if test "$relay_unknown" -ge 3; then
        printf 'Agent Relay status is unreadable; check that the app is running, then retry sign-in.\n' >&2
        exit 1
      fi
      ;;
  esac
  relay_interval=$(printf '%s' "${sign_in:-}" | jq -r '.data.interval // empty' 2>/dev/null)
  sleep "${relay_interval:-5}"
done
```

## 4. Apply the agent-led defaults and register this session

Read the current sharing mode before changing it. `/setup/status` reports it as
`.data.sharing_mode` (`new`, `all`, or `selected`; `new` when nothing is
uploading yet). The probe reports the same field, `sharing_mode`, in
`agent-relay-probe status --account <account-id> --workspace <workspace-id> --json`.

```sh
relay_prior_mode=$(curl -sS --max-time 60 --unix-socket "$relay_socket" \
  http://relay/setup/status | jq -r '.data.sharing_mode // empty')
case "$relay_prior_mode" in
  new|all|selected) printf 'sharing_mode=%s\n' "$relay_prior_mode" ;;
  *) printf 'Could not read the sharing mode; ask the human before changing sharing.\n' >&2
     exit 1 ;;
esac
```

Continue only with a verified `new`, `all`, or `selected`. If the status read
fails or reports no mode, retry once; if it is still unknown, stop and ask the
human how to proceed rather than sending any sharing request.

On a fresh install, or when it already reports `new`, keep `new`. When an
existing install reports `all` (which also uploads past sessions, so it is
broader than this skill's default) or `selected`, tell the human the current
mode and ask whether to keep it or change it to `new`. Never change it silently.
Only when the human explicitly chooses a different mode, set
`relay_sharing_mode` to that answer at the top of the next block; left unset,
the block keeps the mode the app reports, so nothing changes without an answer.
Remember the chosen mode: section 8 checks it again, and shell variables do not
survive between separate tool calls.

Then set and verify all three agent-led defaults even when the app's `/setup/*`
bootstrap already applied them. Sharing mode `new` uploads every new session;
auto-activation puts every existing live session and every future session on
the relay; direct delivery sets Claude Code `crossSessionInbound` to `accept`
by default, the same default the app's first-run setup applies.

The default never overrides a value the human set: `{"default":true}` writes
`accept` only when `~/.claude/settings.json` does not contain the
`crossSessionInbound` key, and answers the human's own value as `user_choice`.
A present key with any value (`hold`, `refuse`) is an explicit choice: keep it,
report it, and continue setup. Only POST `{"enabled":true}` over it when the
human asks for direct delivery after being told. A build older than this
default answers `invalid_enabled`; there the skill reads the settings file
itself and POSTs `{"enabled":true}` only after proving the key is absent (no
file, or exactly one JSON object without the key) or already `accept`, so an
older build keeps an explicit choice too. A file that exists but cannot be read or parsed (blank,
broken, not one object) proves nothing, and neither does a value other than
`hold`, `refuse` or `accept` (even `""`): stop and ask the human rather than
writing over it. Only `hold` and `refuse` are kept choices this skill acts on:

```sh
# Unset keeps the current mode; set it only to the human's explicit answer.
if test -z "${relay_sharing_mode:-}"; then
  relay_sharing_mode=$(curl -sS --max-time 60 --unix-socket "$relay_socket" \
    http://relay/setup/status | jq -r '.data.sharing_mode // empty')
fi
case "$relay_sharing_mode" in
  new|all|selected) ;;
  *) printf 'Sharing mode unknown; ask the human before changing sharing.\n' >&2
     exit 1 ;;
esac
# absent, unreadable, or present:<value> for crossSessionInbound in the
# user's Claude settings. Only a regular file holding exactly one JSON object
# is readable (a dangling symlink or a directory is not); key presence is
# tracked apart from its value ("" included). setting-up-agent-relay-sessions
# section 2 mirrors this helper; change both together.
claude_inbound_choice() {
  settings_file="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
  if [ ! -e "$settings_file" ] && [ ! -L "$settings_file" ]; then echo absent; return; fi
  if [ ! -f "$settings_file" ]; then echo unreadable; return; fi
  jq -rse 'if length == 1 and (.[0] | type) == "object"
    then (.[0] | if has("crossSessionInbound")
      then "present:" + (.crossSessionInbound | if type == "string" then . else tojson end)
      else "absent" end)
    else error("not one JSON object") end' "$settings_file" 2>/dev/null || echo unreadable
}
file_choice=$(claude_inbound_choice)
direct_delivery=$(curl -sS --max-time 60 --unix-socket "$relay_socket" -H 'Content-Type: application/json' \
  -d '{"default":true}' http://relay/setup/direct-delivery)
if printf '%s\n' "$direct_delivery" | jq -e '.error.code == "invalid_enabled"' >/dev/null; then
  case "$file_choice" in
    absent|present:accept)
      direct_delivery=$(curl -sS --max-time 60 --unix-socket "$relay_socket" -H 'Content-Type: application/json' \
        -d '{"enabled":true}' http://relay/setup/direct-delivery) ;;
    present:hold|present:refuse)
      direct_delivery=$(jq -nc --arg choice "${file_choice#present:}" \
        '{ok: true, data: {direct_delivery: false, user_choice: $choice}}') ;;
    *)
      printf 'Claude Code settings are unreadable or name crossSessionInbound as something other than hold, refuse or accept (%s), so delivery was left unchanged; ask the human.\n' \
        "$file_choice" >&2
      exit 1 ;;
  esac
fi
printf '%s\n' "$direct_delivery" | jq
kept_choice=$(printf '%s\n' "$direct_delivery" | \
  jq -r 'select(.ok and .data.direct_delivery == false) | .data.user_choice // empty |
    select(. == "hold" or . == "refuse")')
if printf '%s\n' "$direct_delivery" | jq -e '.ok and .data.direct_delivery == false and
    .data.user_choice != null and (.data.user_choice | IN("hold", "refuse", "accept") | not)' >/dev/null; then
  printf 'Your crossSessionInbound is set to an unrecognized value, so delivery was left unchanged; ask the human.\n' >&2
  exit 1
fi
if [ -n "$kept_choice" ]; then
  case "$kept_choice" in
    hold) effect='relayed messages to Claude Code wait for your approval' ;;
    refuse) effect='relayed messages to Claude Code are dropped' ;;
  esac
  printf 'Kept your own crossSessionInbound value "%s": %s.\n' "$kept_choice" "$effect" >&2
elif ! printf '%s\n' "$direct_delivery" | jq -e '.ok and .data.direct_delivery'; then
  if printf '%s\n' "$direct_delivery" | jq -e '.error.code == "managed_policy"' >/dev/null; then
    printf 'Organization-managed Claude settings forbid direct delivery.\n' >&2
  fi
  exit 1
fi
curl -sS --max-time 60 --unix-socket "$relay_socket" -H 'Content-Type: application/json' \
  -d "$(jq -nc --arg mode "$relay_sharing_mode" '{mode:$mode}')" \
  http://relay/setup/sharing | jq
curl -sS --max-time 60 --unix-socket "$relay_socket" -H 'Content-Type: application/json' \
  -d '{"enabled":true}' \
  http://relay/setup/auto-activate | jq
register=$(curl -sS --max-time 60 --unix-socket "$relay_socket" \
  -H 'Content-Type: application/json' \
  -d '{}' http://relay/register)
printf '%s\n' "$register" | jq
registration_status=$(curl -sS --max-time 60 --unix-socket "$relay_socket" \
  http://relay/setup/status)
printf '%s\n' "$registration_status" | \
  jq '{session: .data.session, direct_delivery: .data.direct_delivery, error}'
printf '%s\n' "$registration_status" | jq -e --arg kept "$kept_choice" \
  '.ok and (.data.session.id | type == "string" and length > 0) and
   .data.session.registered == true and
   (.data.session.direct_delivery == true or
    (.data.session.direct_delivery == false and $kept != ""))'
```

**Codex on macOS.** The system `curl` hides a Codex session's identity from the
socket there, so `/register` is refused `not_a_relay_session` (section 6 says
the same for subscriptions). In a Codex session on macOS, find `relay_probe`
with section 6's first block and send the registration through it instead of
the `curl` line above, then read `/setup/status` as above:

```sh
register=$(printf '{}' | "$relay_probe" relay socket-request \
  --socket "$relay_socket" --method POST --path /register)
```

If `/setup/sign-in` is refused the same way, send its payload through the probe
the same way. Without a probe, stop and report it rather than registering
another process.

Direct delivery is checked first so a managed-policy refusal stops setup
before auto-activation or upload settings are changed. Report that refusal
clearly; never claim the three-default setup completed. A kept explicit
choice is not a refusal: report it as "direct delivery kept off at your
`crossSessionInbound` value" (with `hold`, relayed messages wait for the
human's approval; with `refuse`, they are dropped) and finish the rest of
setup.

Sharing mode and auto-activate are different settings: the former
controls upload eligibility and the latter controls Relay registration. The
setup flow requires both.

`POST /register` discovers the nearest live Codex or Claude ancestor. A session
inside tmux over SSH is valid. Never pass a session id or token in the body.
The default name includes a 128-bit session-derived suffix, so dozens of
sessions in the same checkout receive distinct `name@direct` addresses. Save
and report the address from `/setup/status` for each session; never reuse one
session's address for another. `/register` may omit the session id or
direct-delivery state, so its response alone is not verification. Require the
status response to show this session's non-empty id, `registered:true`, and
`session.direct_delivery:true`, or `session.direct_delivery:false` exactly
when the human kept their own `crossSessionInbound` (a missing field is never
success), as above. Do not override a kept choice to satisfy this check; only
POST `{"enabled":true}` when the human says yes after being told. An app fix is expected to add those
fields to `/register`, but the status check remains authoritative.

## 5. Create and test the webhook

**When a kept choice holds this session's messages.** If `/setup/status`
shows `session.direct_delivery:false` because the human kept their own
`crossSessionInbound` (a Claude Code session; Codex is unaffected), the
injection checks in sections 5–7 cannot complete by themselves. With `hold`,
the test message arrives as a held message: ask the human to approve it, then
confirm the marker and message id as below. With `refuse`, it is dropped:
create the webhook or subscription if asked, but report its injection test as
unverified because of the kept choice, rather than waiting for a marker that
cannot arrive.

Perform this section only when the human requested a webhook (or explicitly
asked for the full end-to-end setup). Otherwise skip it; webhook creation is a
persistent external mutation, not an install prerequisite.

Capture the one-time secret without echoing it:

```sh
umask 077
hook_file=$(mktemp)
curl -sS --max-time 60 --unix-socket "$relay_socket" -H 'Content-Type: application/json' \
  -d '{}' http://relay/webhooks >"$hook_file"
jq '{ok, created: .data.created, webhook_id: .data.webhook_id, url: .data.url, error}' "$hook_file"
```

When `created:true`, send a unique marker. Initialize every temporary path so
cleanup can name and delete exact files without `-f` or globs:

```sh
hook_config=
marker=
webhook_post_ok=false
webhook_post_status=0
if test "$(jq -r '.data.created // false' "$hook_file")" = true && \
   test "$(jq -r '.data.secret // empty' "$hook_file")" != ''; then
  hook_url=$(jq -r '.data.url' "$hook_file")
  hook_secret=$(jq -r '.data.secret' "$hook_file")
  hook_config=$(mktemp)
  chmod 600 "$hook_config"
  printf 'header = "Authorization: Bearer %s"\n' "$hook_secret" >"$hook_config"
  marker="agent-relay-webhook-test-$(date +%s)"
  if curl -fsS --config "$hook_config" -X POST "$hook_url" \
      -H 'Content-Type: application/json' \
      -d "{\"text\":\"$marker\",\"source\":\"agent-driven-setup\"}"; then
    webhook_post_ok=true
  else
    webhook_post_status=$?
  fi
else
  printf 'Webhook already exists; its original secret is required to repeat delivery verification.\n' >&2
fi
if test -n "$hook_config" && test -e "$hook_config"; then
  rm -- "$hook_config"
fi
if test -e "$hook_file"; then
  rm -- "$hook_file"
fi
unset hook_secret
if test "$webhook_post_ok" = true; then
  printf 'Webhook test sent; end this turn and wait for marker: %s\n' "$marker"
elif test -n "$marker"; then
  printf 'Webhook test POST failed with curl status %s; do not wait for delivery.\n' \
    "$webhook_post_status" >&2
  exit "$webhook_post_status"
fi
```

Do not claim success merely because the POST returned 2xx, and do not sleep or
poll for the injected message during the same turn. Relay injects only after
the session becomes idle between turns. After the POST and exact-file cleanup,
end the current turn by saying that setup is waiting for the exact marker. On
the next turn, confirm that the injected body contains that marker and its
Agent Relay header contains a full message id; record that id in the final
evidence. If a polling consumer is part of the setup, also observe that same id
there before declaring the webhook test complete. If the webhook already
existed, its secret is correctly returned as null; do not revoke or recreate it
without human authorization. Ask for the existing secret or report that the
delivery test could not be repeated.

## 6. Subscribe requested integrations

Require the provider and repository from the human. A GitHub pull request needs
one resource glob, its number (`pulls/NUMBER/**`). Relayfile matches the PR's
reviews, review comments, conversation comments and check runs to that glob by
the PR they reference, so no other glob is needed: nothing is written under
`pulls/NUMBER/reviews/**`, `pulls/NUMBER/status/**` or
`issues/NUMBER/comments/**`, and subscriptions on those never deliver. This
works on every released Desktop and is the path to use today. If a review or
comment then never arrives, read it back in relayfile at its own path (the
table in `subscribing-relay-webhooks-and-writebacks`): a 404 means the event
never reached relayfile
([relayfile-cloud#317](https://github.com/AgentWorkforce/relayfile-cloud/issues/317)),
not that the subscription is wrong.

First find the Desktop's `agent-relay-probe`. macOS needs it for Codex sessions
(below) and it is the client for the one-command form once that ships:

```sh
relay_probe=
for candidate in \
  "$HOME/.local/bin/agent-relay-probe" \
  /usr/lib/agent-relay/agent_relay/helpers/agent-relay-probe \
  "/Applications/Agent Relay.app/Contents/Helpers/agent-relay-probe"
do
  if test -x "$candidate"; then relay_probe=$candidate; break; fi
done
if test -z "$relay_probe" && test -d "$HOME/.local/lib/agent-relay"; then
  relay_probe=$(find "$HOME/.local/lib/agent-relay" -type f -perm -u+x \
    -path '*/agent_relay/helpers/agent-relay-probe' -print -quit 2>/dev/null)
fi
printf 'relay_probe=%s\n' "${relay_probe:-none}"
```

A running Desktop keeps `~/.local/bin/agent-relay-probe` at its own version; the
other candidates are the copies bundled by the `.deb`, the macOS app, and the
per-user tarball. Then subscribe through the socket. On macOS use the probe
client when there is one: the system `curl` hides a Codex session's identity
there and is refused `not_a_relay_session`, so a Codex session with no probe
stops instead. `curl` is correct for Claude Code and on Linux:

```sh
# One client for every socket request in this section and in undo: the probe
# on macOS (system curl hides a Codex session there), curl elsewhere.
relay_req() {  # METHOD PATH [JSON body]
  if test "$(uname -s)" = Darwin && test -n "${relay_probe:-}"; then
    printf '%s' "${3:-}" | "$relay_probe" relay socket-request \
      --socket "$relay_socket" --method "$1" --path "$2"
  elif test "$(uname -s)" = Darwin && test -n "${CODEX_THREAD_ID:-}"; then
    printf 'A Codex session on macOS needs agent-relay-probe; start Agent Relay and retry.\n' >&2
    return 1
  elif test -n "${3:-}"; then
    curl -sS --max-time 60 --unix-socket "$relay_socket" -X "$1" \
      -H 'Content-Type: application/json' -d "$3" "http://relay$2"
  else
    curl -sS --max-time 60 --unix-socket "$relay_socket" -X "$1" "http://relay$2"
  fi
}

# This PR's subscriptions, in whichever form they were made. Fails, rather than
# answering "none", when the status cannot be read: both callers stop then.
pr_url='https://github.com/OWNER/REPO/pull/NUMBER'
pr_resources() {
  relay_status=$(relay_req GET /setup/status) || return 1
  # -s reads the whole body as one list, so an empty body is an error too.
  printf '%s\n' "$relay_status" | jq -rs --arg url "$pr_url" \
    --arg short 'OWNER/REPO#NUMBER' \
    --arg pulls '/github/repos/OWNER/REPO/pulls/NUMBER/' \
    --arg issues '/github/repos/OWNER/REPO/issues/NUMBER/' '
    if length == 1 and .[0].ok == true
       and (.[0].data.integrations | type) == "array" then
      .[0].data.integrations[] | select(.provider == "github") | .resource
      | select(. == $url or . == $short or startswith($url + "/")
               or startswith($pulls) or startswith($issues))
    else
      error("Agent Relay status is unreadable; no subscription was changed.")
    end'
}

held=$(pr_resources) || exit 1
if printf '%s\n' "$held" | grep -Fqx -e "$pr_url" -e 'OWNER/REPO#NUMBER'; then
  # The one-command subscription is a single complete request: nothing to add.
  printf 'Already subscribed:\n%s\n' "$held"
elif test -z "$held" && test -n "${relay_probe:-}" && \
     "$relay_probe" relay --help 2>&1 | grep -q '^  subscribe'; then
  # The one-command form (below), only for a PR with no subscription yet and
  # only when the installed probe has it.
  "$relay_probe" relay subscribe "$pr_url" | jq
else
  relay_req POST /integrations/subscribe \
    "$(jq -nc --arg resource '/github/repos/OWNER/REPO/pulls/NUMBER/**' \
      '{provider:"github",resource:$resource}')" | jq
fi
relay_req GET /setup/status | jq -c '.data.integrations'
```

Require `.ok` and `.data.subscribed` to be `true`, and the entry in the status
list to be `ready: true`. A repeated request returns HTTP 409
`conflict` ("already subscribed"). That is success only for a binding made on
or after 2026-09-19. Earlier bindings predate PR-identity matching
(relayfile-cloud#237, relaycast#447), match paths literally and miss reviews
and comments. If the binding may be older, or you cannot tell, recreate it
once: `relay_req DELETE /integrations/subscribe` with the same body, then
subscribe again. Verified on
Desktop 2026.10.6 with both the probe client and `curl`.

**One-command form (not yet released).** `relay-desktop#216` adds
`agent-relay-probe relay subscribe 'https://github.com/OWNER/REPO/pull/NUMBER'`
(or `OWNER/REPO#NUMBER`), which makes one numeric `pulls/NUMBER/**` subscription
that the Desktop correlates with the PR's reviews, comments, and checks. It is
absent from Desktop 2026.10.6, where it fails with `unrecognized subcommand
'subscribe'`, so do not lead with it. Use it only when the installed probe
advertises it. The block above makes that choice, and first checks the
session's existing subscriptions. A pull request already subscribed with the
one-command form is left alone. A pull request already holding
the number glob answers 409 `conflict` on a repeat; recreate that binding once
if it may predate 2026-09-19 (see above). Extra `reviews`, `status` or
`issues/NUMBER/comments` globs from an earlier version of this skill never
deliver; unsubscribe them.

Once released, re-test it before relying on its details; the behavior described
in that PR (a repeat answering `"already_subscribed":true`, `--remove` to end it,
and a numeric subscription covering reviews, comments and checks) has not been verified here.

Refusals and what they mean:

- `not_a_relay_session`, "Register this session before subscribing it": run
  section 4's `POST /register` first.
- `not_a_relay_session`, "already managed by this computer's relay" from
  `/register`: this session was started by the Agent Relay fleet broker, not
  the Desktop. Use the workspace CLI below instead.
- `refused` or `busy`: an earlier subscription is still being made or cleaned
  up; repeat the command shortly.

A fleet-spawned session, or an operator subscribing a named agent, uses the
`agent-relay` CLI with `--to @AGENT_NAME` (a spawned worker's name is in
`RELAY_AGENT_NAME`). **`agent-relay` 13.0.0 is the minimum.** Older CLIs let
the worker's ambient `RELAY_AGENT_TOKEN` displace the workspace key and fail
with `Workspace key required (rk_live_...)` even though `RELAY_WORKSPACE_KEY`
is set:

```sh
pull_glob='/github/repos/OWNER/REPO/pulls/NUMBER/**'
# Shell variables do not survive between tool calls: when relay_cli is unset
# here, find the installed CLI again with section 2's helper, so an installed
# CLI is not replaced by the npx fallback.
# The installed Agent Relay CLI: user-level npm, mise or nvm, then PATH;
# never the .deb's /usr/bin/agent-relay Desktop launcher. Section 2 and
# section 6 define this identically; change both together.
find_relay_cli() {
  for candidate in \
    "$HOME/.local/bin/agent-relay" \
    "$HOME/.npm-global/bin/agent-relay" \
    "$HOME/.agentworkforce/relay/bin/agent-relay"
  do
    if test -x "$candidate" && test "$candidate" != /usr/bin/agent-relay; then
      printf '%s\n' "$candidate"; return
    fi
  done
  for relay_root in \
    "$HOME/.local/share/mise/installs/node" \
    "$HOME/.nvm/versions/node"
  do
    test -d "$relay_root" || continue
    candidate=$(find -L "$relay_root" -mindepth 3 -maxdepth 3 \
      -path '*/bin/agent-relay' -type f -perm -u+x -print -quit 2>/dev/null)
    if test -n "$candidate" && test "$candidate" != /usr/bin/agent-relay; then
      printf '%s\n' "$candidate"; return
    fi
  done
  candidate=$(command -v agent-relay 2>/dev/null || true)
  if test -n "$candidate" && test -x "$candidate" && test "$candidate" != /usr/bin/agent-relay; then
    printf '%s\n' "$candidate"
  fi
}
test -n "${relay_cli:-}" || relay_cli=$(find_relay_cli)
relay_cli_major=
if test -n "${relay_cli:-}"; then
  relay_cli_major=$("$relay_cli" --version 2>/dev/null | sed -n 's/^[^0-9]*\([0-9][0-9]*\)\..*/\1/p' | sed -n '1p')
fi
if test -n "$relay_cli_major" && test "$relay_cli_major" -ge 13; then
  "$relay_cli" integration subscribe github \
    --resource "$pull_glob" --to '@AGENT_NAME' --no-input
elif test -n "$relay_cli_major"; then
  # 12.x and older: keep the ambient agent token from displacing the key.
  env -u RELAY_AGENT_TOKEN "$relay_cli" integration subscribe github \
    --resource "$pull_glob" --to '@AGENT_NAME' --no-input
else
  # No usable installed CLI: run the fixed release without installing it.
  npx -y agent-relay@13 integration subscribe github \
    --resource "$pull_glob" --to '@AGENT_NAME' --no-input
fi
```

Use the `relay_cli` found in section 2 (re-discovered above when this runs in
a new shell), never a bare `agent-relay`: on a
`.deb` host `/usr/bin/agent-relay` is the Desktop launcher, and `PATH` may
resolve to it or to a different, older CLI than the one whose version was
checked.

The CLI path follows `orchestrating-agent-relay`, which subscribes the same
single `pulls/NUMBER/**` glob. Never put the workspace key in argv.

Make the subscribe step a required post-create step in the agent's PR
workflow: whenever it authors a PR, it subscribes its own live session
immediately, without waiting for an operator to name the PR. This is the
authored-PR pattern from `orchestrating-agent-relay`; it covers every future PR
the agent authors without subscribing unrelated repository traffic. The
Desktop removes the subscription itself after the PR closes or merges.
For real GitHub verification, use a harmless event the human authorized
on that PR (for example, a test conversation comment and a requested bot
review). After causing the event, do not wait or poll within the active turn:
end the turn saying which event marker is expected. Confirm the injected event
and record its full message id on the next turn after Relay has delivered it.
Do not create a comment, review, or rerun without authorization. A successful
subscribe response alone does not prove end-to-end delivery. With a kept
`hold` or `refuse`, follow the note at the start of section 5.

## 7. Coexist with a REST-polling MCP

Do not disable an existing polling consumer. Desktop injection acknowledges
only this registered agent's delivery queue and does not advance a separate
workspace consumer's cursor. A polling MCP should use its own identity/cursor,
not this session agent's `/v1/deliveries` token.

Injected text contains both the full Relaycast message id and a short `ref`.
When polling and injection can surface the same workspace message, deduplicate
on the full message id before acting. During verification, leave the poller
running and observe the test message there. Then end the current turn so Relay
can inject it, and on the next turn confirm the same full id arrived while the
agent acted only once.

## 8. Final verification and report

Read status again:

```sh
curl -sS --max-time 60 --unix-socket "$relay_socket" http://relay/setup/status | \
  jq '{version: .data.version, sign_in: .data.sign_in, workspace: .data.workspace, sharing_mode: .data.sharing_mode, auto_activate: .data.auto_activate, direct_delivery: .data.direct_delivery, direct_delivery_user_choice: .data.direct_delivery_user_choice, defaults_error: .data.defaults_error, uploader: .data.uploader, session: .data.session, webhook: .data.webhook, integrations: .data.integrations}'
```

Require all three defaults before declaring setup complete. Set
`relay_sharing_mode` again to the mode chosen in section 4 (`new`, or the
existing mode the human kept); the check refuses to guess it. This block runs
in a fresh shell after the turns of sections 5–7, so it also derives the
kept direct-delivery choice again: from status's
`direct_delivery_user_choice`, or from the settings file on a build older
than that field:

```sh
: "${relay_sharing_mode:?set relay_sharing_mode to the mode chosen in section 4}"
# absent, unreadable, or present:<value> for crossSessionInbound in the
# user's Claude settings. Only a regular file holding exactly one JSON object
# is readable (a dangling symlink or a directory is not); key presence is
# tracked apart from its value ("" included). setting-up-agent-relay-sessions
# section 2 mirrors this helper; change both together.
claude_inbound_choice() {
  settings_file="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
  if [ ! -e "$settings_file" ] && [ ! -L "$settings_file" ]; then echo absent; return; fi
  if [ ! -f "$settings_file" ]; then echo unreadable; return; fi
  jq -rse 'if length == 1 and (.[0] | type) == "object"
    then (.[0] | if has("crossSessionInbound")
      then "present:" + (.crossSessionInbound | if type == "string" then . else tojson end)
      else "absent" end)
    else error("not one JSON object") end' "$settings_file" 2>/dev/null || echo unreadable
}
final_status=$(curl -fsS --max-time 60 --unix-socket "$relay_socket" http://relay/setup/status)
kept_choice=$(printf '%s\n' "$final_status" | \
  jq -r 'select(.data.direct_delivery == false) | .data.direct_delivery_user_choice // empty |
    select(. == "hold" or . == "refuse")')
# A build older than direct_delivery_user_choice: read the settings file.
if [ -z "$kept_choice" ] && printf '%s\n' "$final_status" | \
    jq -e '.data.direct_delivery == false and (.data | has("direct_delivery_user_choice") | not)' >/dev/null; then
  final_choice=$(claude_inbound_choice)
  case "$final_choice" in
    present:hold) kept_choice=hold ;;
    present:refuse) kept_choice=refuse ;;
    *) printf 'Direct delivery is off and no kept hold or refuse could be confirmed (%s); do not enable it here, ask the human.\n' \
         "$final_choice" >&2 ;;
  esac
fi
printf '%s\n' "$final_status" | jq '{direct_delivery: .data.direct_delivery, direct_delivery_user_choice: .data.direct_delivery_user_choice}'
printf '%s\n' "$final_status" | \
  jq -e --arg mode "$relay_sharing_mode" --arg kept "$kept_choice" '.ok and .data.sharing_mode == $mode and
    .data.auto_activate == true and
    (.data.direct_delivery == true or (.data.direct_delivery == false and $kept != ""))'
```

Report the exact version, `signed_in`, signed-in workspace id and name, sharing
mode (`new`, or the existing mode the human kept), auto-activate `true`,
direct-delivery `true` (or the human's kept `crossSessionInbound` value),
uploader health, session address, direct-delivery state, webhook test marker,
subscriptions, real GitHub event evidence, and polling-coexistence result.
Separate verified facts from steps that still require a human or external
event.

## Recovery and undo

- **Socket missing or slow:** check `systemctl --user status agent-relay.service`
  on Linux or `open -a "Agent Relay"` on macOS. Confirm the pointer names a
  socket. Never delete a live socket; restart the owning service/app. A reply
  that takes up to about 30 seconds is the known latency in section 1
  (relay-desktop#333), not a fault: allow 60 seconds and retry once.
- **`not_a_relay_session`:** the command is not a descendant of a supported
  live Codex/Claude session, or the session id/process start no longer matches.
  Run it through this agent's shell tool. In tmux, confirm the agent process and
  shell share the pane's process tree.
- **Sign-in blocked by a legacy uploader** (`sign_in: error`, message "An older
  managed history uploader is active. Stop it explicitly before enabling this
  probe."): the probe counts any of these legacy `ai-hist push` registrations as
  active, even when idle: the file
  `~/Library/LaunchAgents/com.ai-hist.push.plist` (even if unloaded), a loaded
  launchd label `com.ai-hist.push`, or a crontab line containing
  `# ai-hist push (managed)`. Check each, then with the human's approval unload
  and move aside (do not delete) what exists. Create the backup directory first,
  use a unique no-clobber destination, and confirm the move before retrying:

  ```sh
  # bootout exits non-zero when the label was never loaded; that is fine, so
  # judge success by whether the label is still loaded, not by its exit code.
  launchctl bootout "gui/$(id -u)/com.ai-hist.push" 2>/dev/null || true
  if launchctl list | awk '{print $NF}' | grep -qx com.ai-hist.push; then
    echo 'com.ai-hist.push is still loaded; do not retry sign-in' >&2
    exit 1
  fi
  plist="$HOME/Library/LaunchAgents/com.ai-hist.push.plist"
  if test -e "$plist"; then
    backup_dir="$HOME/.agentworkforce/backup"
    mkdir -p "$backup_dir"
    mv -n "$plist" "$backup_dir/com.ai-hist.push.plist.bak-$(date +%Y%m%d%H%M%S)"
    test ! -e "$plist" || { echo 'plist was not moved; do not retry sign-in' >&2; exit 1; }
  fi
  ```

  If instead the marked cron line is the cause, back up the full crontab first
  and remove only that line, so the change can be reversed with
  `crontab <backup-file>`:

  ```sh
  mkdir -p "$HOME/.agentworkforce/backup"
  cron_backup="$HOME/.agentworkforce/backup/crontab.bak-$(date +%Y%m%d%H%M%S)"
  crontab -l > "$cron_backup" && test -s "$cron_backup" || { echo 'crontab backup failed; do not edit it' >&2; exit 1; }
  grep -vF '# ai-hist push (managed)' "$cron_backup" | crontab -
  ! crontab -l | grep -qF '# ai-hist push (managed)' || { echo 'cron line still present; do not retry sign-in' >&2; exit 1; }
  ```

  Leave `com.ai-hist.sync` alone; it is not checked. Then re-run
  `/setup/sign-in` with the same payload.
- **`not_signed_in`:** run `/setup/sign-in`; do not paste tokens into the
  request. Restart after `expired` or `denied` to obtain a new code.
- **Older schedules could not be inspected:** inspect the user's legacy cron,
  launchd, or user-service upload schedules first. Only after that inspection,
  retry `/setup/sign-in` with the original payload plus
  `"acknowledge_uninspected_schedules":true`. This flag records a completed
  safety check; never send it merely to silence an error.
- **`not_allowed` from `/register`:** enable the app's self-registration
  setting. Do not bypass it with workspace credentials.
- **Managed Claude policy:** if direct delivery reports `managed_policy`, the
  organization controls `crossSessionInbound`; report the policy block rather
  than modifying managed settings.
- **Undo registration:** `curl -sS --max-time 60 --unix-socket "$relay_socket" -X DELETE http://relay/register | jq`.
- **Undo an integration:** with section 6's `relay_probe`, `relay_socket`,
  `relay_req`, and `pr_resources` defined, remove every subscription the
  session holds for the pull request, in whichever form it was made. On macOS
  `relay_req` uses the probe, and a Codex session without one stops instead of
  sending a `curl` request that cannot be identified:

  ```sh
  held=$(pr_resources) || exit 1
  printf '%s\n' "$held" | while IFS= read -r resource; do
    test -n "$resource" || continue
    relay_req DELETE /integrations/subscribe \
      "$(jq -nc --arg resource "$resource" '{provider:"github",resource:$resource}')" | jq
  done
  relay_req GET /setup/status | jq -c '.data.integrations'
  ```

  Require the final list to hold none of that pull request's resources.
- **Undo a webhook:** `curl -sS --max-time 60 --unix-socket "$relay_socket" -X DELETE http://relay/webhooks | jq`.
- **Undo direct delivery:** POST `{"enabled":false}` to
  `/setup/direct-delivery`. On Claude this writes `"crossSessionInbound":
  "hold"` (Claude Code's own default) on current builds, an explicit choice
  that no later default turns back on; older builds removed the key instead,
  so there set `"hold"` by hand to keep it off. Managed organization policy
  still wins.
- **Undo sharing:** POST the human's prior mode (`selected`, `new`, or `all`) to
  `/setup/sharing`. Ask before changing it when the earlier value is unknown.
- **Auto-activate accidentally enabled:** POST `{"enabled":false}` to
  `/setup/auto-activate`. This does not remove existing registrations.
- **Uninstall:** stop/disable the user service before removing the Linux
  package, or quit the Mac app before removing it. Do not delete app data or
  revoke webhooks unless the human explicitly asks for data removal.

---

# Part 3: Sessions

# Set Up Agent Relay Session Handoff

Drive the whole onboarding from this agent's own shell and MCP tools. Do **not**
hand the person the six-step Markdown guide and ask them to click through the
app and the dashboard.

Prefer setting this up with the Agent Relay agent at
<https://arelay.to/agent-relay>, which checks your current state and confirms
each step. Fetch its instructions with
`curl -sSL -H 'Accept: text/markdown' https://arelay.to/agent-relay`.

The finished state is:

- the desktop app is installed, signed in to the **intended workspace**, the
  uploader is healthy, and this session is registered with direct delivery on,
  or with the person's own `crossSessionInbound` (`hold` or `refuse`) kept and
  reported (see section 2; delivery is never turned on without their yes);
- the **agent-sessions cloud MCP** is installed in this project and loaded by
  this session, so the handoff tools (`list_relay_agents`, `send_relay_message`,
  `read_relay_conversation`, `check_relay_inbox`, `get_shared_session`,
  `get_shared_session_context`, `search_shared_sessions`) are available;
- a live round trip is proven when a teammate is online: this agent messages the
  teammate's agent, **reads the teammate's real session back** with
  `get_shared_session_context`, and the teammate reads this session and replies.
  When no teammate is online yet, the fallback is the roster plus this person's
  own uploaded session reading back cleanly — reported as self-verified, not as a
  proven two-way handoff.

## Requires

This skill builds on `setting-up-agent-relay-desktop` (verified install, sign-in,
registration, agent-led defaults). If that skill is **not installed** in this
agent, stop, tell the person, and have them install the companions together,
then restart the agent. Do not improvise the install or sign-in from memory.

- prpm: `npx prpm install collections/agent-relay-setup --as claude --global -y`
- skills.sh: `npx skills add https://github.com/AgentWorkforce/skills --skill setting-up-agent-relay-desktop --skill setting-up-agent-relay-sessions`
- Codex: do not use prpm's `--as codex` (it drops paragraphs and writes under `~/.agents/skills`, which Codex does not load as its global skill folder). Mirror both installed `SKILL.md` files into `~/.codex/skills/<name>/` with the loop in **Install this skill (and its dependency)** below.

## The only human steps

Everything else is automatable; be honest that these are not. **Ask for all of
them up front, before touching anything** — the token in particular is needed
only at section 3, but minting it and exporting it into the launching
environment takes the person a few minutes, so have them start it while the
desktop install and sign-in run:

1. **Approve one Google device-login link** — only when no reusable `agent-relay`
   CLI/desktop login exists. A reused login needs zero clicks.
2. **Provide one Bearer token** minted in the dashboard's **"Connect your agent"**
   card. No setup route returns this token by design. **Do not have the person
   paste it into this conversation** — the uploader shares this session's prompts
   with teammates, so a pasted secret would become teammate-readable. Instead the
   person puts it into the **environment that launches their agent** (or an OS
   keychain / a mode-0600 file) via a shell *they* run, and this agent references
   it only by variable name — never by value. See **Token handling** below.
3. **Same workspace, both sides.** The teammate (or workspace admin) must have
   invited this person's email to the **shared** workspace — two agents can only
   reach each other inside one workspace. Confirm the workspace id matches on
   both machines; do not guess it.

One structural note so expectations are right: there is **no automatic ack** to
the sender when a handoff is picked up, and **no partial session sharing** — a
session is on-or-off on the relay. Confirm a handoff by activity, not a push.

## How this composes with `setting-up-agent-relay-desktop`

This skill is a thin layer on a working desktop. The desktop skill owns install,
sign-in, defaults and self-registration; this one owns only the agent-sessions
cloud MCP and the live handoff. Section 2 checks the desktop prerequisite and,
if it is not met, invokes `/setting-up-agent-relay-desktop` to completion and
returns here. Keep the `/setup/*` details in that one skill so a contract change
updates it once.

## Install this skill (and its dependency) for Codex and Claude Code

When the task includes installing this skill, install its canonical Claude
package **and** the desktop skill it depends on, then mirror both unchanged
`SKILL.md` files where Codex loads global skills:

```sh
for pkg in setting-up-agent-relay-sessions setting-up-agent-relay-desktop; do
  npx prpm install "@agent-relay/$pkg" --as claude --global -y
  codex_skill="$HOME/.codex/skills/$pkg"
  mkdir -p "$codex_skill"
  install -m 0644 "$HOME/.claude/skills/$pkg/SKILL.md" "$codex_skill/SKILL.md"
  cmp "$HOME/.claude/skills/$pkg/SKILL.md" "$codex_skill/SKILL.md"
done
```

With skills.sh instead, use the command under **Requires**; it installs into each agent's skills folder directly, so the manual Codex mirror below is only for the prpm route.

Do not use prpm's `--as codex` conversion for these packages: it writes under
`~/.agents/skills`, which Codex does not load as its global skill folder, and it
drops explanatory paragraphs from the Markdown workflow. Confirm both final
paths explicitly before continuing.

## Why ordering matters

MCPs load at agent **startup**. The agent-sessions MCP must be installed
**before** the session that will use it starts. A session already running when
the MCP was added has no handoff tools and must be **restarted**. Install the
MCP (section 3) before relying on the tools (sections 4–5); if this very session
predates the MCP, say so and restart it rather than reporting missing tools as a
failure.

## Token handling

The token must never land in the agent conversation, in a Relay message, in
process argv, or in the repo. Three places leak it, and this skill avoids all
three:

- **The chat** — this session is uploaded and teammate-readable, so a token
  pasted to the agent leaks to the workspace. The person sets it themselves (see
  below); the agent only ever sees the variable **name**.
- **Process argv** — a token interpolated into `claude mcp add --header
  "... Bearer $TOKEN"` is visible to `ps` and gets written literally into
  `~/.claude.json`. Pass the unexpanded placeholder `${AGENT_RELAY_SESSIONS_TOKEN}`
  instead; Claude Code expands `${VAR}` in MCP headers from the environment at
  launch, so the stored config holds only a reference and argv holds only the
  literal `${…}`. **This expansion exists only in Claude Code ≥ 2.1.119** — an
  older client sends the placeholder literally and auth fails, so check the
  version first (section 3) and update Claude rather than inlining the token.
- **The launching environment** — both CLIs read the token from an environment
  variable (`${VAR}` for Claude, `--bearer-token-env-var` for Codex), so the
  variable must exist in the environment that **launches the agent**, not in a
  transient shell-tool subprocess (which cannot alter its parent or a later
  process).

Use the single variable name `AGENT_RELAY_SESSIONS_TOKEN` throughout. Keep the
one-time entry separate from durable supply:

- **One-time, into the current shell** (non-echoing, run by the person, never
  the agent): `read -rs AGENT_RELAY_SESSIONS_TOKEN && export AGENT_RELAY_SESSIONS_TOKEN`,
  then launch the agent from *that* shell. Do **not** add this `read` to a shell
  profile — every shell that sources the profile would block waiting for input.
- **Durable across restarts:** store the token in the OS keychain or a mode-0600
  file, and have the launcher export it **non-interactively**, e.g.
  `export AGENT_RELAY_SESSIONS_TOKEN="$(security find-generic-password -s agent-relay-sessions -w)"`
  (macOS keychain) or from a 0600 file. Put that line in a dedicated launcher the
  person starts the agent from — not an interactive prompt in the profile.

The agent never prints the value; verify presence only with
`test -n "${AGENT_RELAY_SESSIONS_TOKEN:-}"`.

## 0. Preflight: collect the human inputs first

Before section 1, tell the person what they need and ask for it in one message:

1. the **shared workspace id** (or the teammate/admin who invited them);
2. a **Bearer token** minted in the dashboard's "Connect your agent" card for
   that workspace, stored by *them* as `AGENT_RELAY_SESSIONS_TOKEN` in the
   environment that launches their agent (see **Token handling** — never pasted
   into this chat);
3. the **teammate's relay address** for the round trip, if one will be online.

Before sending that message, check what this machine already has. If an Agent
Relay CLI 13.x is installed (a user-level npm, mise or nvm `agent-relay`, not
the `.deb`'s `/usr/bin/agent-relay` Desktop launcher), run these read-only
checks. Without one, section 2 reads the signed-in workspace from
`/setup/status` instead. The CLI is optional for this skill: detect it as
below, which skips the Desktop launcher that `command -v agent-relay` finds on
a `.deb` install, and do not install or upgrade it just for these checks.

```sh
relay_cli=$(command -v agent-relay || true)
case "$relay_cli" in ''|/usr/bin/agent-relay) relay_cli= ;; esac
relay_cli_version=
if test -n "$relay_cli"; then
  relay_cli_version=$("$relay_cli" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
fi
# Gate only these optional checks on 13.x; keep relay_cli itself, which
# setting-up-agent-relay-desktop section 6 uses with its own version handling.
relay_cli_checks=
case "$relay_cli_version" in
  13.*) relay_cli_checks=1 ;;
  *) test -n "$relay_cli" && printf 'agent-relay %s is not 13.x; read the workspace from /setup/status instead.\n' "${relay_cli_version:-unknown}" >&2 ;;
esac
if test -n "$relay_cli_checks"; then
  "$relay_cli" cloud whoami       # signed in, and as whom
  "$relay_cli" workspace active   # the active Cloud workspace (keys stay masked)
  "$relay_cli" cloud workspaces   # every workspace this login can use, with ids
  "$relay_cli" status             # workspace, cloud login and local broker
fi
```

If the active workspace is already the shared one, confirm it with the person
instead of asking them to look it up. Never switch to or create another
workspace unless they explicitly ask.

Check presence without printing the value:

```sh
test -n "${AGENT_RELAY_SESSIONS_TOKEN:-}" && echo token=present || echo token=missing
```

A missing token does **not** block sections 1–3: the MCP config stores only a
`${AGENT_RELAY_SESSIONS_TOKEN}` reference, never the value, so it can be added
now. Do the desktop work and add the MCP, then have the person restart the agent
**once**, from an environment that exports the token, so that single restart both
loads the MCP and supplies the credential. Sections 4–5 are gated on that
restart. If the person would rather not defer, they can export the token and
restart first, which then costs a second restart only if the MCP was added after.

## 1. Confirm the host and this session

Run every command from this agent's own shell tool, and only from a **genuine
interactive** Claude Code or Codex session. The app identifies the caller from
kernel credentials and resolves its process ancestry against discovered live
sessions, so a command from an unrelated terminal — or from a non-interactive
`codex exec` / `claude -p` run — is rejected with `not_a_relay_session`. (This
is verified: a plain SSH shell and a `codex exec` invocation are both refused;
an interactive session is admitted.)

```sh
uname -s; uname -m
test -n "${SSH_CONNECTION:-}" && printf 'transport=ssh\n' || true
test -n "${TMUX:-}" && printf 'session=tmux\n' || true
```

Define the request helper once per shell. It finds the private setup socket
without printing private files and uses the desktop's `agent-relay-probe` on
macOS (the system `curl` hides a Codex session's identity there and is refused
`not_a_relay_session`), `curl` elsewhere, and stops with a message for a macOS
Codex session that has no probe:

```sh
pointer="$HOME/.agentworkforce/desktop/relay-socket"
if test -r "$pointer"; then relay_socket=$(sed -n '1p' "$pointer"); fi
if test -n "${relay_socket:-}" && ! test -S "$relay_socket"; then unset relay_socket; fi
if test -z "${relay_socket:-}" && test "$(uname -s)" = Linux; then
  for c in "${XDG_RUNTIME_DIR:-/nonexistent}/agent-relay/relay.sock" "/run/user/$(id -u)/agent-relay/relay.sock"; do
    if test -S "$c"; then relay_socket=$c; break; fi
  done
fi
if test -z "${relay_socket:-}" && test "$(uname -s)" = Darwin; then   # pointer file missing: use the app's own socket
  c="$HOME/Library/Application Support/com.agentrelay.desktop/run/relay.sock"
  if test -S "$c"; then relay_socket=$c; fi
fi
relay_probe=
for c in "$HOME/.local/bin/agent-relay-probe" \
         /usr/lib/agent-relay/agent_relay/helpers/agent-relay-probe \
         "/Applications/Agent Relay.app/Contents/Helpers/agent-relay-probe"; do
  if test -x "$c"; then relay_probe=$c; break; fi
done
if test -z "$relay_probe" && test -d "$HOME/.local/lib/agent-relay"; then   # per-user tarball
  relay_probe=$(find "$HOME/.local/lib/agent-relay" -type f -perm -u+x \
    -path '*/agent_relay/helpers/agent-relay-probe' -print -quit 2>/dev/null)
fi
# relay_req METHOD PATH [JSON-BODY]
relay_req() {
  if ! test -S "${relay_socket:-/nonexistent}"; then
    echo 'No Agent Relay socket found; start Agent Relay (the desktop) and retry.' >&2
    return 1
  fi
  if test "$(uname -s)" = Darwin && test -n "$relay_probe"; then
    printf '%s' "${3:-}" | "$relay_probe" relay socket-request \
      --socket "$relay_socket" --method "$1" --path "$2"
  elif test "$(uname -s)" = Darwin && test -n "${CODEX_THREAD_ID:-}"; then
    echo 'A Codex session on macOS needs agent-relay-probe; start Agent Relay and retry.' >&2
    return 1
  elif test -n "${3:-}"; then
    # body on stdin: curl treats a -d/--data-binary value starting with @ as a filename
    printf '%s' "$3" | curl -sS --max-time 60 --unix-socket "$relay_socket" -X "$1" \
      -H 'Content-Type: application/json' --data-binary @- "http://relay$2"
  else
    curl -sS --max-time 60 --unix-socket "$relay_socket" -X "$1" "http://relay$2"
  fi
}
```

**Slow socket replies are known.** `GET /setup/status` and `GET /agents` can
take about 30 seconds on a busy Desktop (tracked in
AgentWorkforce/relay-desktop#333). Allow 60 seconds per call (add
`--max-time 60` to the `curl` lines above), and do not treat a slow reply as a
failure. The probe's `relay socket-request` can give up sooner with "Probe
could not finish. Check your connection and run setup again"; on these paths
that is the same latency, not a setup problem. Retry once before reporting a
blocker, and do not rerun desktop setup because of it.

## 2. Make sure the desktop is set up first

Session handoff needs a signed-in, healthy desktop underneath it. If the socket
is missing, or `/setup/status` returns `404`, or sign-in/uploader/registration
are not in the finished state below, **invoke the `setting-up-agent-relay-desktop`
skill** (`/setting-up-agent-relay-desktop`; if it is not installed, see **Requires**), let it run to completion, and then
return to section 2's verification. Do not re-implement its install, sign-in, or
`/setup/*` steps here — that skill is the single source of truth for them, and
this skill resumes once it reports a registered, signed-in session.

Verify the desktop state this skill depends on:

```sh
relay_req GET /setup/status | jq '{
  version: .data.version,
  sign_in: .data.sign_in,
  workspace: .data.workspace,
  sharing_mode: .data.sharing_mode,
  uploader: .data.uploader,
  direct_delivery: .data.direct_delivery,
  session: .data.session,
  defaults_error: .data.defaults_error
}'
```

Require all of these before continuing, and stop with a clear message if any
fails. `/setup/status` reports sign-in as the string `sign_in` (match the desktop
skill), and the flag that actually governs whether **this** session's handoff
replies arrive live is the session-level `session.direct_delivery`, not the
top-level default. Direct delivery is on by default, but the desktop keeps a
`crossSessionInbound` value the person set themselves (`hold` or `refuse`), and
so does this skill: such a kept choice is reported in
`direct_delivery_user_choice` (read from the settings file on a desktop build
older than that field) and is accepted here only when the top-level flag is
explicitly `false` (a missing field is never success). Any other value is not
a kept choice this skill acts on:

```sh
# absent, unreadable, or present:<value> for crossSessionInbound in the
# user's Claude settings. Only a regular file holding exactly one JSON object
# is readable (a dangling symlink or a directory is not); key presence is
# tracked apart from its value ("" included). Mirrors
# setting-up-agent-relay-desktop sections 4 and 8; change them together.
claude_inbound_choice() {
  settings_file="${CLAUDE_CONFIG_DIR:-$HOME/.claude}/settings.json"
  if [ ! -e "$settings_file" ] && [ ! -L "$settings_file" ]; then echo absent; return; fi
  if [ ! -f "$settings_file" ]; then echo unreadable; return; fi
  jq -rse 'if length == 1 and (.[0] | type) == "object"
    then (.[0] | if has("crossSessionInbound")
      then "present:" + (.crossSessionInbound | if type == "string" then . else tojson end)
      else "absent" end)
    else error("not one JSON object") end' "$settings_file" 2>/dev/null || echo unreadable
}
setup_status=$(relay_req GET /setup/status)
kept_choice=$(printf '%s\n' "$setup_status" | \
  jq -r 'select(.data.direct_delivery == false) | .data.direct_delivery_user_choice // empty |
    select(. == "hold" or . == "refuse")')
if [ -z "$kept_choice" ] && printf '%s\n' "$setup_status" | \
    jq -e '.data.direct_delivery == false and (.data | has("direct_delivery_user_choice") | not)' >/dev/null; then
  case "$(claude_inbound_choice)" in
    present:hold) kept_choice=hold ;;
    present:refuse) kept_choice=refuse ;;
  esac
fi
printf '%s\n' "$setup_status" | jq -e --arg kept "$kept_choice" '
  .ok
  and .data.sign_in == "signed_in"
  and (.data.workspace.id | type == "string" and length > 0)
  and .data.uploader.healthy == true
  and (.data.session.id | type == "string" and length > 0)
  and .data.session.registered == true
  and ((.data.direct_delivery == true and .data.session.direct_delivery == true)
       or (.data.direct_delivery == false and $kept != ""
           and (.data.session.direct_delivery | type == "boolean")))'
```

**When the person kept their own choice** (`kept_choice` is set): do not
change it, and do not re-run desktop setup to "fix" it, since that keeps it
too. `crossSessionInbound` is Claude Code's setting: when this session reports
`session.direct_delivery:true` (a Codex session), report the kept value and
continue normally; the round trip completes as usual. When it reports
`session.direct_delivery:false` (this Claude Code session), tell them what it
means for the handoff:

- `hold`: relayed messages, including the teammate's handoff reply, arrive as
  held messages they must approve in Claude Code before this agent sees them.
  Setup continues; section 5's round trip completes once they approve the held
  reply.
- `refuse`: relayed messages to this session are dropped, so the round trip
  cannot complete here. Setup continues through section 4, then stops at
  section 5 as **blocked on the person's choice**.

Either way, offer the opt-in once: "Turn on direct delivery for Claude Code?
It sets `crossSessionInbound` to `accept` in your Claude Code settings." Only
on an explicit yes, POST `{"enabled":true}` to `/setup/direct-delivery` and
re-run this check; on no, keep their value and continue as above.

Confirm `workspace.id` is the **shared** workspace both sides agreed on. A
healthy uploader is the one non-obvious prerequisite: for a teammate's agent to
read **this** person's session, this machine must have uploaded it. If
`uploader.healthy` is false (paused, or a failed cycle), fix it before promising
anyone can read this session — this is the single most common reason a live
handoff silently fails.

`sharing_mode` decides which sessions upload at all, and a healthy uploader
does not prove that **this** session is among them. `all` includes it. `new`
includes sessions that started after `new` was enabled, so a session that was
already running when the desktop was set up may not be uploaded. Under
`selected`, only the sessions the person chose upload. Unless the mode is
`all`, tell the person that this session may not be shared yet, and ask them to
select it in the app or to continue from a new session; do not change the
sharing mode or select sessions on their behalf. Section 5's round trip is the
proof: if the teammate cannot read this session there, this is the first
thing to check.

## 3. Install the agent-sessions cloud MCP (before the session uses it)

Check whether the token is present in the environment **without printing it**
(see **Token handling**). It is not required to *add* the MCP, since only a
reference is stored, but it is required for the tools to authenticate after the
restart. If it is missing, add the MCP anyway, tell the person plainly that the
tools will fail auth until they restart the agent from an environment that
exports `AGENT_RELAY_SESSIONS_TOKEN`, and do not report setup complete:

```sh
if ! test -n "${AGENT_RELAY_SESSIONS_TOKEN:-}"; then
  echo 'AGENT_RELAY_SESSIONS_TOKEN is not set here; the MCP will be added by reference, but the agent must be restarted from an environment that exports it.' >&2
fi
```

Detect which agent CLI this session runs and install accordingly.

Claude Code — pass the **unexpanded** placeholder so the token stays out of argv
and out of `~/.claude.json`; Claude expands `${VAR}` in MCP headers from the
environment at launch. This expansion requires **Claude Code ≥ 2.1.119**, so
verify the version first and update rather than inlining the token on an older
client. Single-quote the header so the shell does not expand it here. Default
scope is `local` (this project only), which is what you want:

```sh
claude_ver=$(claude --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if test -z "$claude_ver" || test "$(printf '%s\n2.1.119\n' "$claude_ver" | sort -V | head -1)" != 2.1.119; then
  echo "Claude Code $claude_ver does not expand \${VAR} in MCP headers (need >= 2.1.119); update Claude Code before adding this server." >&2
  exit 1
fi
claude mcp add --transport http agent-relay-sessions \
  https://agentrelay.com/cloud/api/v1/mcp/shared-sessions \
  --header 'Authorization: Bearer ${AGENT_RELAY_SESSIONS_TOKEN}'
claude mcp list | grep -F agent-relay-sessions
```

Codex — `codex mcp add` records the **variable name**, not the value, and writes
to user-level `~/.codex/config.toml`, so the server is enabled for **all** Codex
projects (Codex has no project-scoped MCP). Confirm the person is OK with
user-wide access before adding it:

```sh
codex mcp add agent-relay-sessions \
  --url https://agentrelay.com/cloud/api/v1/mcp/shared-sessions \
  --bearer-token-env-var AGENT_RELAY_SESSIONS_TOKEN
codex mcp list | grep -F agent-relay-sessions
```

Because MCPs load at startup, the running session must be **restarted** to pick
up the server — and because both CLIs read the token from the environment at
launch, the restarted process must **inherit** `AGENT_RELAY_SESSIONS_TOKEN`.
That is why the variable belongs in the shell profile / launcher, not a transient
shell-tool subprocess: an `export` in this agent's shell tool cannot reach the
parent or a freshly launched session. After the restart, re-verify presence with
`test -n "${AGENT_RELAY_SESSIONS_TOKEN:-}"` before using the tools, and do not
report the tools as missing without first confirming the session started *after*
the MCP was added and with the variable set.

## 4. Confirm the handoff tools loaded

In the session that started after the MCP was added, confirm the tools are
present before using them. List this session's MCP tools and check for the
agent-sessions set; the authoritative check is that the tools actually appear to
this agent:

```sh
# Claude Code: the tools surface as mcp__agent-relay-sessions__*
claude mcp list | grep -F agent-relay-sessions
```

Then call `list_relay_agents` through the MCP (an MCP tool call, not a shell
command). A successful call that returns the workspace roster proves the MCP is
loaded, the token is valid, and this session can see the shared workspace. If
the call errors with auth, the token is wrong or for another workspace — have
the person re-mint it from the correct workspace's dashboard.

## 5. Prove one live round trip

The real proof is agent-to-agent, so it needs a counterpart. Pick the path that
matches what is available:

**A teammate's agent is online (full round trip).** Ask the person for the
teammate's relay address (e.g. `@manav`), then:

1. `list_relay_agents` — confirm the teammate's address is listed.
2. **Prove the reverse read path first:** call `get_shared_session_context` on
   the teammate's session and confirm it returns their **real** content (prompts/
   events + a dashboard citation link). If it errors or is empty, their uploader
   is unhealthy — stop and have them fix it; do not declare GREEN, because this
   side cannot actually read their work yet.
3. `send_relay_message` — send a short, uniquely-marked message to that address.
4. The teammate's agent reads this session with `get_shared_session_context` and
   continues; have them reply through the relay.
5. GREEN requires **both directions**: this agent read the teammate's real
   content (step 2) **and** the teammate's reply lands here. There is no
   automatic ack — confirm by the reply/activity, not a push. Because Relay
   injects an inbound message only when the session is idle between turns, **end
   the turn** after sending and confirm the reply on the next turn; do not sleep
   or poll inside one turn.
6. Only when this session reports `session.direct_delivery:false` (a Claude
   Code session with a kept choice): with `hold`, the reply arrives as a held
   message; ask the person to approve it, then confirm it on the next turn.
   With `refuse` and no yes to the opt-in, the reply is dropped: report setup
   as **blocked on the person's choice**, naming the opt-in, not as complete,
   and not as failed. A session reporting `true` follows steps 1–5 unchanged.

**No teammate online yet (self-verify the read path).** Prove the machinery
without a second person:

1. `list_relay_agents` returns the roster — MCP + token + workspace are good.
2. `search_shared_sessions` / `get_shared_session_context` return this person's
   **own** recently uploaded session (real prompts/events plus a dashboard
   citation link) — the uploader→read path works.
3. Record that the two-party round trip still needs the teammate online, and
   name it as the remaining step rather than claiming the handoff is proven.
   Setup is not complete until the full round trip succeeds.

Never paste the Bearer token or any session token into a `send_relay_message`
body or any artifact.

## 6. Report

Separate verified facts from what still needs a human or a second machine:

- app version, `signed_in`, the signed-in **workspace id and name**;
- uploader `healthy`, this session's `agent@machine` address, and direct
  delivery on, or the person's kept `crossSessionInbound` value and what it
  does (held for approval, or dropped), with the opt-in they were offered;
- agent-sessions MCP installed and its tools confirmed loaded in this session;
- `list_relay_agents` roster seen; and either the full round-trip evidence (the
  teammate's real content + reply) or the self-verified read path plus the named
  remaining step (teammate online).

Do not call the setup complete on a 2xx from `claude mcp add` alone, and do not
count a roster listing as a proven handoff.

## Recovery

- **`not_a_relay_session` from `/setup/*`:** the caller is not inside a discovered
  interactive session. Run it from this agent's own shell inside an **interactive**
  Claude Code or Codex session — a non-interactive `codex exec` or `claude -p` run
  is not admitted, and neither is an unrelated terminal. In tmux/SSH, confirm the
  agent process and shell share the pane's process tree.
- **No handoff tools in the session:** the MCP was added after the session
  started, or at the wrong scope. Reinstall at project scope (section 3) and
  **restart** the session; MCPs load only at startup.
- **`list_relay_agents` auth error:** the token is wrong, expired, or from a
  different workspace. Because the MCP config stores only a **reference** to
  `AGENT_RELAY_SESSIONS_TOKEN`, fixing it means updating the **value in the
  launch environment** (keychain / 0600 file) and **restarting** the agent so it
  re-reads it — re-running `claude mcp add` / `codex mcp add` alone changes
  nothing. Re-mint from the intended workspace's "Connect your agent" card if the
  token itself is bad. On Claude, also confirm the client is ≥ 2.1.119 (older
  clients send the `${VAR}` placeholder literally).
- **Teammate's address missing from the roster:** they are not on the **same**
  workspace, or their session is not on the relay. Have them register their
  session and confirm the shared workspace id.
- **Can't read the teammate's session:** their **uploader** is not healthy
  (paused or a failed cycle). Have them check `/setup/status` shows
  `uploader.healthy == true`, not "Sync paused/offline".
- **Message arrives held, not live:** direct delivery is off for the recipient
  (Codex delivers directly by default). If their `/setup/status` shows a
  `direct_delivery_user_choice`, they chose it: tell them, and POST
  `{"enabled":true}` to `/setup/direct-delivery` on their machine only when
  they say yes. Otherwise apply the default the way
  `setting-up-agent-relay-desktop` section 4 does: `{"default":true}`, which
  never overrides a choice, and on an older build that answers
  `invalid_enabled`, its settings-file check before any `{"enabled":true}`. A managed Claude policy can block this; report it
  rather than editing managed settings.
- **Remove the MCP:** `claude mcp remove agent-relay-sessions` /
  `codex mcp remove agent-relay-sessions`. This removes only the handoff tools;
  it does not sign the desktop out or stop uploads.

## Relationship to the other skills

- **`setting-up-agent-relay-desktop`** — the layer below this one: install,
  sign-in, agent-led defaults, self-registration. Use it when section 2 is not
  yet satisfied.
- **`relay-connect`** — a lighter, account-free, time-boxed way for two agents to
  share one session via a hand-over link. Prefer it for an ad-hoc pairing;
  prefer this skill for durable teammate-to-teammate handoff inside a shared
  workspace.
