From e2a
Enables email-based remote control of a Claude Code session during long-running tasks. The agent sends status updates and picks up replies via email, so you can steer work while AFK.
How this skill is triggered — by the user, by Claude, or both
Slash command
/e2a:tetherThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
> **Beta.** The transport (e2a) and the flow are real; polish (adaptive
Beta. The transport (e2a) and the flow are real; polish (adaptive backoff, richer digests) is still open.
/tether keeps you connected to a working session over email. The agent emails
you updates when it judges there's something worth reporting (not on a
timer, not every turn), and checks your inbox on an interval so your replies —
questions or new instructions — are picked up within a few minutes. Reply
stop to end.
Transport primer. tether rides on e2a. The e2a operate-well manual — the
e2askill (${CLAUDE_PLUGIN_ROOT}/skills/e2a/SKILL.md) — carries the mental model this skill assumes: how threading really works (reply headers + stable subject, notconversation_id), when a sharedagents.e2a.devaddress is all you need vs. a custom domain, and thepending_review/HITL gotcha. Read it if you're new to e2a; tether does not re-explain those.
Coding agents are turn-based; nothing native listens to an inbox mid-session. So the two directions use different mechanisms:
tether.sh update "…" at meaningful
moments (finished a slice, hit a blocker, needs a decision). Cadence is the
model's judgment → no per-turn spam, nothing to throttle.tether.sh listen — it blocks
on the e2a CLI's WebSocket (e2a listen --once; no LLM tokens while waiting)
for the duration set at start --for, so replies are picked up within
seconds; if the WebSocket is unavailable it degrades automatically to
interval polling. It wakes the agent only when a reply actually arrives (or
the window ends). See Durability tiers for keeping it alive across
idle/sleep.tether.sh ask "<question>", which emails the question into the thread and
blocks until you reply, then prints your answer. This is the hard rule:
while tethered, every question goes over email, never the terminal.Notification hook emails you when the
agent is stuck on a permission prompt it can't proceed past. Note: an emailed
reply cannot answer a CLI permission prompt (there's no native way to inject
approval), so for unattended runs pre-authorize the tools the session needs
(a permission allowlist / a less-prompting mode). The hook is the safety net,
not the approval channel.Prerequisites: Node (the transport is the
e2aCLI ≥ the version pinned inlib.sh— resolved from$E2A_CLI, then PATH, then fetched automatically vianpx -y @e2a/cli@^MIN, so there is nothing to install by hand) and Python 3 (local state handling only).
tether.sh setupIf the e2a CLI is logged in (e2a login in a browser, or e2a login --with-key on a headless box), one command does the whole bootstrap:
"${CLAUDE_PLUGIN_ROOT}/skills/tether/tether.sh" setup
It verifies the credential (e2a whoami), ensures a tether inbox (reusing an
existing tether-… agent or creating one on the shared domain — it will
not silently adopt a non-tether inbox), turns outbound review off on
that inbox, mints a least-privilege agent-scoped key (e2a_agt_…), and
writes ~/.e2a-tether.env (previous file kept as .bak). Every step fails
hard — it never stores the broad account key and never reports "ready" with a
half-working config. Flags: --email you@yourdomain to use/create a specific
inbox, --new to force a fresh one.
e2a_agt_… — least privilege, so a leaked
key can't touch the rest of the account): from the CLI with
e2a keys create --agent <inbox>, or from https://e2a.dev/api-keys.
No domain? Create the agent on the shared agents.e2a.dev domain
(e2a agents create [email protected] — live immediately, no DNS).cp "${CLAUDE_PLUGIN_ROOT}/skills/tether/tether.env.example" ~/.e2a-tether.env
chmod 600 ~/.e2a-tether.env # fill E2A_API_KEY (e2a_agt_…) + E2A_AGENT_EMAIL
"${CLAUDE_PLUGIN_ROOT}/skills/tether/install.sh" --to <repo-root>
Credentials resolve in order: explicit env vars → ~/.e2a-tether.env →
~/.e2a/config.json (written by e2a login; after e2a login --agent <inbox>
that file already holds a least-privilege agent key, so tether needs no
tether-specific config at all).
The tether agent must have send-side protection / HITL off, or each update is held for review instead of reaching you.
tether.shnow detects this on every send: a held intro makesstartrefuse to arm, and a heldupdate/askprints aHELD for review (pending_review)warning and exits non-zero — so a held session fails loudly instead of "reporting" into a queue the user never sees.
Before tethering, confirm the harness can actually send. Run "$T" status
(where T is defined below). If it reports config: MISSING, this is a
first-time user — help them get to a working setup instead of just failing:
e2a login — or, when e2a isn't installed globally, the npx form:
npx -y @e2a/cli login (same auto-fetch the harness itself uses). It opens
the browser sign-up/sign-in and saves an account-scoped key to
~/.e2a/config.json. (Headless box: they mint a key in the dashboard and
run … login --with-key.) Interactive sign-in is theirs to complete: hand
them the command, don't drive it."$T" setup — it creates the inbox, disables
outbound review, mints the agent-scoped key, and writes
~/.e2a-tether.env. See Setup above for what it refuses to do."$T" status should now print config: OK (agent …). Proceed
to the runtime flow.If status already prints config: OK, skip this — they're a returning user.
Don't put a configured user through onboarding.
/tether is invoked)Let T="${CLAUDE_PLUGIN_ROOT}/skills/tether/tether.sh".
"$T" status. If config: MISSING, do First run (new
user) above before continuing — don't call start and let it error out."$T" start <email> --title "<work>" --for <duration> (or
--until <ISO>; omit both for until-stop) — sends the intro, opens the
thread, arms, records the window. --title is required (start refuses
without it): a short description of the work being done (e.g. "migrate loft → @e2a/ui", "fix webhook retries") — it becomes the thread's subject
line (Tether: <repo> — <title>), which is how the user tells this session
apart from others in their inbox. The subject is fixed at start (threading
needs it stable), so title the work, not the first step. --for takes a single
unit (30m, 2h, 8h, 1d); a compound
like 1h30m is rejected rather than silently treated as no-limit. If the intro
comes back held for review (pending_review), start refuses to arm and
tells you to turn protection off — a held intro means the user never got it."$T" update --html <file> — a plain-text fallback is auto-derived (or pass
--text "<fallback>"). Plain "$T" update "<text>" is for quick one-liner
acks only. To send a file (a rendered PDF, a screenshot, a small log), add
--attach <file> — repeatable, on either form, capped at 15 MB total per
send (exit 3 = file not found, exit 4 = over the cap; past the cap,
upload the file somewhere and send a link instead). Good moments: finished a
slice, made a decision that's worth surfacing, hit a blocker, or before a
long unattended stretch. Skip trivial
turns. If update prints a HELD for review (pending_review)
warning (exit 2), the update did not reach the user — stop and fix
protection before continuing; don't keep "reporting" into a review queue."$T" ask "<question>" (in the background); it emails the question and blocks
until the user replies, then prints the answer. --attach <file> works here
too — attach the artifact the decision hinges on (a diff, a mockup) rather
than describing it. Do not use AskUserQuestion
or a bare terminal prompt while tethered — an AFK user can't answer it and the
session stalls. ask coordinates with listen automatically (it holds a lock
so a background listen pauses and can't swallow your answer). Handle its exit
codes: exit 3 = timed out with no reply (default 30m) — re-ask, send a
nudge update, or keep working and listening, but never fall back to a
terminal prompt; exit 4 = the question was held for review and never
reached the user (fix protection)."$T" listen in the background. It
waits on the CLI's WebSocket (real-time, no tokens while waiting; degrades
to polling if the WS is unavailable) and exits with either:
REPLY_RECEIVED: + the message → act on it (then update with the result),
and relaunch listen for the remaining window; orTETHER_EXPIRED → the window is up; run "$T" stop.
Replies are deduped by message-id and survive e2a's async parse, so none are
dropped or repeated. (poll is the same one-shot check if you want it manually.)stop/done, the window expires, or the work
is complete: "$T" stop.The recipient is a person reading email (often on a phone), not a terminal. Write for that medium, not for a CLI.
HTML (update --html <file> — the default; use it for any substantive update):
max-width:~480px, inline styles
only (email strips <style>/<head>), readable sizes (14–15px), and a
vertical/stacked layout. Avoid wide tables and big ASCII in <pre> — they
force horizontal scroll and shrink to unreadable on phones.<div> boxes, small <table>s) over ASCII art.update auto-derives the
plain-text fallback, so HTML sends are always safe.Plain text (update "<text>" — quick one-liner acks only):
**bold**, `code`, # headings render as literal
characters in a plain-text email. Use plain prose.ask bodies are plain-text only (no --html) — keep questions short
and prose-only there. --attach does work on ask: attach the artifact the
decision hinges on (a diff, a mockup) rather than describing it.Both:
--attach) instead of inlining it.update, never a fresh send. tether.sh
threads by replying (In-Reply-To/References + a stable subject), which is what
Gmail/Outlook actually stitch on. e2a threads on conversation_id, but Gmail
ignores that — so a fresh send in the same e2a conversation still lands as a
second thread in the user's inbox (the split Gmail showed). While tethered,
send every update through "$T" update (it replies into the thread); do not
reach for the e2a MCP send_message or start a new subject to reach the user
mid-session. One session = one thread = one subject.listen/ask block on the e2a CLI's WebSocket wait (no LLM tokens while
waiting), so reply latency is seconds. The poll interval only matters as the
degraded cadence when the WebSocket is unavailable, and as the backfill check
between waits. The agent is only woken (a real turn) when a reply actually
lands.
| env var | default | effect |
|---|---|---|
E2A_TETHER_POLL_INTERVAL | 20 (s) | fallback poll cadence when the WS wait is unavailable |
E2A_TETHER_ASK_TIMEOUT | 1800 (s) | how long ask blocks for an answer before giving up |
E2A_URL | https://e2a.dev | e2a deployment root (set for self-host) |
E2A_CLI | (auto) | override the e2a CLI invocation (e.g. node /repo/cli/dist/bin/e2a.js) |
The only thing that costs a turn per tick is a /loop heartbeat (tier 2
below) — keep that coarse (e.g. 30m).
listen polls for the whole --for window —
automatic while the terminal stays open, and cheap (curl only, no tokens).
This is what the duration setup buys you: one long-lived poller, not manual
restarts. Add listen --awake to keep the machine from idle-sleeping
during the window (macOS caffeinate, auto-released when listening ends).
Note: --awake does not survive closing the lid (macOS clamshell still
sleeps) — that's tier 3./loop (e.g. every 30m) can relaunch
listen if it dies and keep the session warm. /loop wakes the agent (a
full turn each tick) — use it as a supervisor, not the poller.Each start opens a dedicated email thread (fresh conversation id, its own
subject; replies anchor by In-Reply-To), and local state is keyed per repo
(git toplevel), so tethered sessions in different repos coexist without
touching each other's thread, watermark, or ask-lock. Within one repo,
start refuses to arm over a live session instead of silently hijacking
its thread. To run a second session in the same repo, start it with
--parallel: it self-keys a fresh state file and prints a
TETHER_STATE="…" handle — prefix every subsequent tether call in that
session with it (TETHER_STATE="…" "$T" update …), and pass a distinct
--title so the inbox threads are tellable apart. Forgetting the prefix is
warned about (commands notice parallel peers exist) and every send echoes its
thread id, so misdirection is observable. (A pre-existing machine-global
state.json from an older tether keeps working until its session stops —
note that WHILE it exists it shadows repo keying, so it also blocks start
in other repos; stop that session to retire it.)
| file | role |
|---|---|
tether.sh | runtime CLI: setup / start --title [--for] [--parallel] / update [--html] [--attach] / ask [--attach] / listen / poll / status / stop |
lib.sh | config + e2a-CLI resolution (t_cli) + send/reply/wait helpers |
hooks/tether-notify.sh | optional Notification hook (blocked-alert) |
install.sh | wire/unwire the Notification hook; _selftest |
tether.env.example | credentials template |
npx claudepluginhub tokencanopy/e2a --plugin e2aEnables communication with users via Telegram for clarifications, options, blockers, task completion notifications, and long-running task updates instead of terminal.
Sends iMessages for real-time user input and status notifications during Claude Code sessions. Use ask_user to wait for responses on decisions or clarifications; notify_user for non-blocking updates.
Автономный режим работы, когда пользователь AFK. Использовать, когда пользователь вводит `/afk [часы] [бюджет%] [--checkpoint <мин>]` или просит агента продолжать работу самостоятельно. CLI контролирует лимит времени — агент опрашивает `grace afk tick` между шагами и завершает работу, когда CLI это разрешит. Необратимые решения эскалируются в Telegram через `grace afk ask`; всё остальное выполняется, откладывается или откатывается согласно матрице автономии.