Security model

Rusuban runs Claude Code agents on your machine, with your logins, on repos where anyone can open an issue or a PR. This page says what holds the agents, how the dashboard checks who is asking, and what stays at risk.

What is trusted

  • You, the owner: your instance's config.json, your commands, your answers on cards.
  • The rules on each repo's default branch: someone with write access merged them.
  • Nothing from GitHub. Issues, PRs, comments, reviews, diffs and a PR's own copy of the rules are data, never instructions.
  • Not the agents either. They are told the rules and the safety frame, then held by limits that do not depend on their obeying.

Only the server merges

  • Agents have no merge command, and the deny rules forbid the merge commands and APIs.
  • The server merges after its gates, computed on live GitHub state, with --match-head-commit: exactly the commit it checked, or nothing.
  • A PR merged by someone else, and a commit pushed straight to a default branch, are flagged.

The built-in deny rules

Every run (judge, coordinator, worker, task, reply) gets them as --disallowedTools, under either host profile. Your own permissions.deny adds to them; nothing removes them.

What Denied
Merging gh pr merge; any gh api call whose text holds merge or Merge (the REST merge endpoints, GraphQL merges, auto-merge).
Pushing to a default branch git push --all and --mirror; gh api on git/refs/heads/<default branch>; what would skip the pre-push hook below: git push --no-verify, and any command that names hooksPath or GIT_CONFIG. Without that hook (git before 2.31), git push naming a repo's default branch (main, :main, +main, refs/heads/main), on any remote.
The owner's commands rusuban project, task, start, permissions, machine, channel, remote, service, init, mcp, and Rusuban's MCP server (mcp__rusuban). The CLI and the MCP server also refuse to run inside a run.
The instance's state Edits to config.json, config.json.seal, automerge.json, projects/, runs/, hub/, leases/, tasks/ and bin/ in the instance home. Agents keep their own folders there: scratch/, worktrees/, repos/, and work/ by default.
The dashboard and your files, from the shell Any shell command that names the dashboard's port (:<port>) or a tailnet address (.ts.net), config.json next to the home's name, automerge.json, answers.jsonl or pending.json. A speed bump, not a wall: see the residual risks.

A denied call is refused, and the run's log says so.

No push to a default branch

Every git a run starts takes Rusuban's hooks (core.hooksPath, set through GIT_CONFIG_COUNT). Before anything leaves, git hands the pre-push hook the URL and the refs it resolved, so a remote's name, a URL, a refspec, an alias or a script cannot hide where a push goes.

  • Refused: a branch named like a project repo's default branch, on every remote. While a repo's default branch is not read yet, main and master too.
  • The one exception: an open PR's head on a contributor's fork, even named main. GitHub lets a maintainer push there, and it is the contributor's branch, not the project's.
  • The repo's own hooks still run, after Rusuban's, as git would have run them. Only reference-transaction, post-index-change and push-to-checkout do not.
  • A hook that cannot read the instance refuses the push.

Only your settings reach a run

Runs load Claude Code's user settings only (--setting-sources user). A repo's .claude/ settings, with their hooks and permissions, never load: a hook or an allow rule merged into a repo does nothing on your machine. Your own user settings do load: their allow rules add to an allowlist run, and their deny rules and hooks apply. With a person's desktop registered, Rusuban adds one hook of its own: rusuban guard refuses that desktop to a run that does not hold its lease.

The host profile

What agents may run on this machine is yours to choose, in config.json, and no repo, rule or PR changes it. permissions.mode has no default: the config does not load without it.

permissions.mode Agents may run
allowlist What permissions.allow lists, plus rusuban itself. Anything that would ask for approval is refused (--permission-mode dontAsk).
bypass Anything, without asking (--dangerously-skip-permissions), except what a deny rule matches.

Under bypass, an agent has your shell. What your account can do on this machine, an agent can do: read and write your files, use your SSH keys, your gh login and every other tool you are logged into, reach your network. The deny rules are the only limit, and they match command text.

Under allowlist, an allow rule for an interpreter or a build tool (Bash(node *), Bash(npm *), Bash(python *)) is a shell too: it runs whatever code it is given.

The dashboard's identity check

  • Loopback only. The server listens on 127.0.0.1.
  • Every request, reads included, must name this server as its Host: 127.0.0.1:<port>, localhost:<port>, or this machine's tailnet name. That stops DNS rebinding. The tailnet name comes from tailscale status --json, read at start and every 5 minutes.
  • From the tailnet, the caller must be you. The Tailscale-User-Login header, which Tailscale Serve sets from the caller's identity, must equal owner.tailscaleLogin in config.json, case aside. A request without it is refused, and so is every tailnet request while owner.tailscaleLogin is not set.
  • On loopback, a request without that header is the local user.
  • Every write also needs the dashboard's own origin: an Origin of http://127.0.0.1:<port>, http://localhost:<port> or https://<tailnet name>, the x-rusuban: 1 header, and a body under 8 KB. A page on another site cannot send that header without a CORS preflight, which the server never grants.

Remote access goes through Tailscale Serve (tailscale serve --bg --https=443 http://127.0.0.1:<port>), never through Tailscale Funnel or any public address.

The approval bypass

A branch that requires an approving review refuses the server's merges until someone approves, and the agents' PRs, pushed under your account, cannot get your approval. The approval bypass is for that.

  • Off for every project until you run rusuban project bypass <id> on, which sets bypassApproval in config.json. No repo, rule, PR or agent sets it.
  • It merges only what the server merges anyway: with the merge switch armed, every gate green or your merge order given. It then merges with gh pr merge --admin.
  • --admin skips every rule of the branch, so the server first reads the base branch's rulesets, its classic protection in full, and the head's checks. It merges only if:
    • every check the branch requires is on the head, from the app the rule names if it names one, and success in its latest run, checks the locks ignore included;
    • the merge method is one the branch allows, and not merge under a linear-history rule;
    • the branch requires neither a code owner's review nor PRs up to date with it, and no thread is open where it requires them resolved;
    • classic protection requires no signed commits, does not bind administrators, restrict who can push or lock the branch;
    • the branch holds no rule or protection setting the server does not know.
  • Journaled. What stops the bypass is journaled once per head and flagged. Turning it on is journaled and flagged; turning it off is journaled.

What is flagged

These go to the journal and to your inbox's "Flagged for you", for 7 days:

  • a merge the server did not make, and a commit pushed straight to a default branch;
  • a merge GitHub refused, and what stopped the approval bypass;
  • a rules change, with its diff, and a locks.json that does not validate;
  • the approval bypass turned on, and every other setting that widens;
  • a config.json changed outside Rusuban's settings;
  • a task that did not end well.

Residual risks

  • Under bypass, an agent has your shell, and it reads text anyone can write. A prompt injection in an issue, a comment or a diff is a path to your shell. The safety frame tells agents that GitHub content is data; it is a prompt, not a wall.
  • Deny rules match command text. A merge, or a write through GitHub's API, hidden behind a variable, an alias, a script, an interpreter or a raw HTTP call is not matched. A git push is checked where git resolved it, but an agent can still skip the hook: another spelling of --no-verify or of the config, or a git that does not inherit the run's environment. The server flags a merge or a push it did not make, after the fact; it does not prevent it.
  • An agent's shell reaches what the deny rules do not cover. The state's deny rules bind Claude Code's file tools; against the shell there are only the cheap rules above, which match text. The dashboard takes any request from this machine as yours: its identity check stops other machines and web pages, not local processes, which can send any Origin and x-rusuban. Under bypass, a shell command that gets past the rules can write config.json, to add a repo or turn a project's approval bypass on, or call the dashboard, to answer a card (a merge order included), allow a setting's card (one asked through the MCP included) or arm the switch. The daemon journals the dashboard's calls as yours; the bypass turned on is flagged. A config.json changed outside Rusuban's settings is flagged too, with what changed, after the fact: the daemon applies it all the same.
  • The real barrier comes after v1: agents under an OS account of their own, or in Claude Code's sandbox where it exists, out of reach of your files and the dashboard; and a GitHub token reserved for agents, without admin rights. Until then, a repo you do not trust gets the allowlist profile.
  • A worker runs the code it works on, in a worktree, under your account: the repo's tests and scripts, and a contributor's when it rebases, finishes or measures their PR. The rules decide which PRs agents work on; the host profile decides what they may run.
  • Agents act as you on GitHub. They use your gh login: what they post appears under your account, and an agent past the deny rules has every right your token has.
  • A merge order overrides the project's locks for its head; a paused switch still holds it. An agent wrote the card: read it before you pick its merge option.

Tests

Each lock has a test that tries to break it:

  • the gates, and adversarial PRs (the rules edited in the PR, injected instructions, red CI, a moved head, a locked path, a missing proof): test/merge-gate.test.mjs and test/fixtures/prs/;
  • the deny rules, under both profiles: test/agents.test.mjs;
  • the identity check and forged writes: test/server.test.mjs;
  • the approval bypass and the owner's commands: test/bypass.test.mjs.