Security model
junco runs a coding agent with bash and file tools on your machine, then writes to GitHub as you. This page names each trust boundary; the mechanics are in how junco works.
The inbox is a code-execution boundary
Whatever lands in inbox/ runs: the agent gets bash
and file tools against the ticket, and a PR-flow ticket's
## Verification blocks execute as your user. Whoever
writes a ticket acts as you.
git.allowedRepoRoots confines PR-flow tickets to
approved checkout locations; the default [] means any
path on disk:
{ "git": { "allowedRepoRoots": ["~/code"] } }
The sandbox
On by default, agent tool execution is confined with OS-level
isolation — Seatbelt on macOS, bubblewrap on Linux; no container,
works offline. bash runs with a scrubbed environment (credentials
and API keys removed) and no network; the per-ticket
network: true frontmatter opt-in widens egress for
one ticket only. The in-process file tools — read, write, edit,
grep — run under a path-jail to the worktree, so writes land
nowhere else. junco performs every git push and
gh pr create itself; the agent never holds a token,
and the env scrub keeps credentials off the agent plane.
The sandbox fails closed: a missing backend binary stops
execution rather than running unconfined. junco doctor
preflights it. Linux needs bubblewrap installed — or
an explicit opt-out via sandbox.backend: "none" or
sandbox.enabled: false. Policy levers:
junco config list; see
configuration.
The approval gate
In GitHub mode, nothing runs on a labeled issue until the sweep
verifies the labeler holds write access — a failed check means no
dispatch. The plan posts as an ordinary, editable issue comment;
execution requires junco:approved applied by a
verified write-access collaborator, and the approval must postdate
the plan comment's timestamp — a stale approval cannot authorize a
later plan. Execution-ticket frontmatter is built by the bridge,
never from model output or issue text, and any verification error
stops the dispatch: the gate fails closed. Full lifecycle in
the GitHub loop.
github.requireApproval:
false executes as soon as the plan comment exists — the
labeler's verified write access becomes the only human gate. Leave
it true unless every write-access collaborator is
trusted to authorize execution by labeling alone.Untrusted input
Applying the trigger label vouches for the issue: its title and
body become the agent's task. Text written by outside reporters is
untrusted input — junco's dispatch and analyze prompts frame
external issue text as data, not instructions, but framing is not
containment. The durable safeguards sit on the output side: every
PR opens as a draft, assess findings park until
junco assess file, analyze comments park until
junco analyze post. Review the draft PR's diff before
marking it ready — the diff is the review surface, not the issue
thread. Flavors in tickets.
What leaves the machine
Two flows leave the machine: traffic to the configured
OpenAI-compatible inference endpoint (local or hosted), and GitHub
writes under your authenticated gh identity or the
dedicated bot account. No third service sits in the loop. The
CLI's best-effort daily update check queries the npm registry
(updateCheck: false opts out); the daemon itself
never phones home. The health server binds loopback —
127.0.0.1:8787 — by default.
observability.healthHost serves /health
with no authentication: in-flight ticket ids, per-ticket progress,
and daemon metrics become readable to the network.Endpoints, service supervision, and troubleshooting are in operations.