Trust model
Echo runs an autonomous worker with --dangerously-skip-permissions — it can
edit files and run commands in its worktree without asking. That is only safe
because of a specific trust design: the worker is powerful inside its branch
and powerless over what lands. This page explains where the boundaries are, and
is honest about where they aren’t.
Merge authority is off the worker
Section titled “Merge authority is off the worker”The single most important property: a worker cannot merge. It implements the
change, opens a PR (Refs #N), runs its adversarial review, posts the
consolidated comment, files any unfixed findings, labels the PR
echo:merge-ready, and stops. The decision to actually merge is made
elsewhere.
Merging happens on the trusted dispatcher side, in
gated_merge.sh, which runs at the top of every sweep. It
independently re-runs the deterministic gates against the PR’s real changed-file
set and only then squash-merges. The worker is never the thing that decides to
merge — it just signals “ready” with a label.
This is enforced two ways, prompt and mechanism:
-
Prompt. The worker’s system prompt ends its job at “PR open, reviewed” — the merge/escalate decision was removed from it entirely.
-
Mechanism. The worker is launched with an explicit tool denylist so it cannot merge even if the prompt is ignored or injected:
--disallowedTools "Bash(gh pr merge:*)" \"Bash(git push:*origin*main*)" \"Bash(git push:*origin*master*)"Bash(gh pr merge:*)is the load-bearing deny. The twogit push-to-base patterns are defense-in-depth against a worker trying to fast-forward the base branch directly. Deny rules are enforced even under--dangerously-skip-permissions— that is exactly why this is a real block and not just prompt text. Plaingit pushis not denied: the worker must push its own topic branch to open the PR.
Deterministic gates vs. advisory review
Section titled “Deterministic gates vs. advisory review”Echo runs two very different kinds of check, and it treats them very differently.
| Deterministic gates | Adversarial review | |
|---|---|---|
| Produced by | gated_merge.sh (dispatcher) | the worker (LLM) |
| Inspects | the PR’s changed paths + CI status, via gh | its own diff |
| Role | hard precondition — trips → hold for a human | advisory — posted + filed, does not gate |
| Trusted? | Yes | No |
The dispatcher-enforced gates are the actual security boundary. In order,
gated_merge.sh runs:
- Cross-repo sentinel — if the change touched a supporting repo, hold (a human merges the sibling PRs). See Cross-repo changes.
- Auth / tenant-isolation (
sensitive_path_gate.sh --pr <N>) — a changed path matching the auth/isolation patterns holds; an error fetching the diff also holds (fail-safe). - Test coverage (
check_test_coverage.sh <N>) — an untested auth/isolation change holds. - CI status (
gh pr checks <N>) — anything failing or pending holds.
The merge-gates guide documents each gate’s exit codes.
The worker’s adversarial-review findings deliberately do not gate the merge. Why: a worker’s review is LLM output judging LLM output produced in the same run. A compromised, injected, or simply over-confident worker could talk itself into a clean review just as easily as into a merge. A worker’s self-review cannot be its own gate. So the findings are treated as a quality signal — surfaced and filed as follow-up work — while the decision to hold an auth PR for a human is made mechanically, regardless of what the review concluded. This is the honest boundary the design commits to:
Untrusted input handling
Section titled “Untrusted input handling”Everything that enters a run from outside the operator is treated as data, not instructions.
Issue title and body — nonce-fenced
Section titled “Issue title and body — nonce-fenced”An issue’s title and body come from whoever filed it, not from the operator. In the worker prompt they are wrapped between marker lines carrying a per-spawn random nonce:
<<<ISSUE_BODY_UNTRUSTED_a1b2c3d4e5f6a7b8>>>…the issue body, verbatim, as data…<<<END_ISSUE_BODY_UNTRUSTED_a1b2c3d4e5f6a7b8>>>The nonce (openssl rand -hex 8, or $RANDOM$RANDOM$$ as a fallback) is
generated fresh at spawn time, so it is unpredictable to whoever filed the issue.
That closes a real fence-escape hole: with a static delimiter, a body
containing its own closing marker could break out of the fence and have the rest
of its text render as trusted template (a forged heading, an “IGNORE ALL PRIOR
INSTRUCTIONS” line). With an unpredictable nonce, nothing in the body can forge a
matching closing marker. The title is fenced the same way with the same nonce,
and the spawn refuses to launch (after 5 attempts) if the generated nonce
happens to appear in the title or body — it will not proceed with a forgeable
delimiter. The prompt tells the worker to read this content as data and to
escalate, not comply, if it reads like an instruction to skip review, merge
without tests, or mark the item done.
Discord replies — allowlisted, fail-closed
Section titled “Discord replies — allowlisted, fail-closed”Operators steer held items by replying in Discord (e.g. approve to merge a held
PR). route_replies.sh authenticates the reply author against
discord.replyAuthors — a list of Discord user IDs — before any keyword
matching. An unknown author, or an author not on the list, is ignored. The
allowlist is fail-closed: an empty, unset, or unparseable replyAuthors
disables inbound reply routing entirely rather than accepting anyone in the
channel. This is what stops a stranger in the channel from typing “approve” and
merging your code. See Discord ops.
The worker environment — scrubbed of secrets
Section titled “The worker environment — scrubbed of secrets”The detached worker is launched with cloud and GitHub-token secrets unset in its environment:
GH_TOKEN GITHUB_TOKEN GH_ENTERPRISE_TOKENAWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKENGOOGLE_APPLICATION_CREDENTIALSThe worker never deploys (the deploy-watcher does), so cloud credentials are
dropped. GH_TOKEN is unset on purpose: gh then falls back to the CLI’s own
stored auth in ~/.config/gh, so the worker can still open its PR without a
bearer token sitting in its environment. Claude’s own auth is deliberately not
unset — the worker needs it to run.
The honest boundary
Section titled “The honest boundary”Be clear about what this does and doesn’t guarantee. The worker still has the
gh CLI. It can comment on issues, push its topic branch, and add labels — and
because gh uses stored auth, the environment scrub doesn’t remove that ability.
So the GitHub-side denylist and the untrusted-input fencing are defense in
depth, not a hermetic sandbox: a determined injection still has a gh-shaped
surface to poke at.
The guarantee Echo actually makes is narrower and firmer: the worker cannot merge. Merge authority lives on the trusted dispatcher side, behind deterministic gates the worker can’t run, produce, or bypass — and the one command that would let it merge is denied at the tool layer even with permissions skipped. Everything a runaway or injected worker could do stops at an open PR, which a human (or the dispatcher’s own gates) still has to clear.
See also
Section titled “See also”- Merge gates — the deterministic gates in full, with exit codes and the
merge.modeswitch. - Discord ops — the reply allowlist and the
approve→ merge path. - Config reference —
authIsolation,gatedPaths,merge,discord.replyAuthors,review.