junco
contents

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.

warn — keep the inbox on a local disk you own. Never point it at a synced or shared folder others write to — write access to the inbox is code execution as your account.

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.

warn — 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.

warn — a non-loopback 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.