Skip to content
Alpha — private preview. Expect rough edges and breaking changes between releases.

Security model

Agentarivm starts coding agents on your machine over the network. That makes it, honestly named, a remote-code-execution control plane — and the whole design follows from taking that seriously.

The mobile app is an unprivileged client. It must never receive:

  • GitHub private keys or long-lived automation credentials,
  • Claude tokens,
  • shell or SSH credentials,
  • direct access to the agent engine’s loopback API,
  • arbitrary filesystem access.

A local manager service running on your machine owns all credentials, repositories, worktrees, agent calls, git operations, and GitHub API operations. The phone asks; the manager decides and acts.

Two credentials per device, doing different jobs. At pairing, a device receives a client certificate from the manager’s own private CA and a device token. The certificate decides who may open a connection at all — a stranger who finds the port is refused during the TLS handshake, before reaching any route. The token authorises each request, is stored only as a hash, and dies instantly on revocation. The two are bound: a token copied off one phone is useless on another, because the second phone cannot produce the first one’s private key.

A private CA, on purpose. A publicly-trusted certificate is trusted because ~150 certificate authorities can vouch for the name — any of which can be made to issue for it. The manager’s CA vouches for one server and a handful of devices, and nothing else on earth trusts it. For a system with a closed set of clients, that is the stronger property, not a compromise.

Allowlists everywhere. Repositories, base branches, models, effort levels, issue actors, task actions — the app chooses from lists the manager holds. There is no endpoint that accepts an arbitrary path, an arbitrary shell command, or a raw terminal.

Creating a repository is the one GitHub write the manager cannot take back. The app can create a repository under the account gh is signed in as, or any organisation that lets it, behind the same Face ID or passcode prompt as adding one. Everything that could refuse the request is checked before GitHub is asked, because the manager holds no right to delete repositories — and should not — and a failure after the creation says that the repository exists.

Argument arrays, never shell strings. Every subprocess — git, gh, and each agent engine — is invoked with an argument array. Nothing is ever interpolated into a command string, and issue titles, bodies, branch names and GitHub usernames are treated as untrusted input throughout.

Path containment. Every worktree path is canonicalised and checked to remain inside the configured worktree root before anything touches it.

The bullet above is about the readability of commands, not about containment, and the difference is worth stating plainly rather than leaving for you to discover.

An agent — and any command a repository’s own .agentarivm.yml declares — runs as a normal process, as the same user the manager runs as. The 0600 file modes described below keep other accounts out. They do not keep out a process running as the owner. File permissions cannot solve this; only the operating system can.

On macOS, it is the operating system. Agents, reviewers and every repository-declared command run under a Seatbelt profile that denies both reads and writes to:

the state directory the database — every device’s credential hash — the device CA private key, and the prompt files
the secrets directory your account refresh token, and per-project secrets
the agent token files the credential your agents authenticate with
the log directory every task across every project
~/.config/gh, ~/.git-credentials, ~/.config/git/credentials, ~/.netrc the operator’s own GitHub credentials — only once github.pushAccount gives the agent one of its own

The config file itself stays readable, because agentarivm report is how an agent tells you what it is doing and it needs the file to find the socket.

The last row is conditional for a reason worth stating rather than hiding. Step four of an agent’s brief is to open a pull request, and the push authenticates through one of those credentials — so denying them used to mean taking away the job rather than the exposure. github.pushAccount is what changes that: the agent pushes with a token pinned into its worktree and runs gh on GH_TOKEN, so none of them is on the path to anything it needs. Until you configure one, they stay reachable and the manager says so — an agent holding the credential the manager comments with is why github.comments.read is off by default.

This is a deny-list, not a jail, and calling it anything else would repeat the mistake this section exists to correct. An agent inside it still reaches the network, still reads the rest of your home directory, and still spends money. What it cannot do is walk off with the keys to your deployment. The reason it is shaped this way is practical: a deny-by-default sandbox would have to name every path a build needs — Xcode, Homebrew, rustup, the pub cache, the Gradle cache, every JDK — and the first one missing fails a build in a way that reads as a flaky test. Operators turn that off, and a sandbox that is off protects nothing.

On Linux it is bubblewrap, denying the same paths. It is not installed by default — apt install bubblewrap, dnf install bubblewrap — and the manager tells you when it is missing rather than quietly running without it.

And on either platform, the sandbox proves itself at every start. Whether a Linux kernel permits an unprivileged user namespace is a property of the host, not the distribution: Ubuntu restricts it through AppArmor, WSL2 ships its own kernel configuration, and a container can have the syscall blocked while bwrap sits on the PATH looking perfectly installed. Every one of those machines passes a “is the tool present” check and confines nothing.

So the manager does not ask. At startup it writes a canary into a directory the profile denies, runs a sandboxed process, and requires two things: that the process ran, and that it could not read the canary. Only then does it report agents as sandboxed. If the check fails, the manager says so, stops using the sandbox — a wrapper that cannot start would otherwise fail every gate and setup command on the machine — and sandbox.mode: required refuses to start at all.

Two more things hold on every platform:

Repository commands do not get your SSH agent. Provisioning, worktree setup and every verify gate run without SSH_AUTH_SOCK. A forwarded SSH agent cannot be scoped down by the process holding it — a child that has the socket can authenticate as you to every host your keys reach, for as long as it runs — and none of those commands has any reason to push. The agent itself keeps the socket, because pushing its branch is its job. If a component needs a private dependency over SSH, give it a deploy key in that component’s environment rather than your own identity.

The environment is an allowlist, not a filter. A subprocess inherits PATH, HOME, the locale and little else. Device tokens, the Anthropic credential and every PRAETOR_* setting are absent from it — not because they were removed, but because they were never added.

With TLS on and client certificates required, an unpaired stranger probing the port can reach exactly two things:

Endpoint Why it is open
/api/v1/health Liveness. It reveals nothing a stranger could not learn by watching the port answer.
/api/v1/pair A never-paired phone has no certificate yet; this is the one door in. It is protected by a one-time code minted on the machine’s own terminal, and rate-limited.

Everything else requires a valid client certificate and a valid device token.

Your code, prompts and transcripts stay on the machine — except what the agents themselves send to their providers, under your agreement with them. The manager’s own outbound traffic is short:

To What When
GitHub clones, pushes, pull requests and comments, through gh as the work needs
the licensing service your account, an anonymous machine id, the version about once a day
the push relay, then Apple a phone’s push token, which kind of event happened, and the notification encrypted to a key only that phone holds when work stops for you
dl.agentarivm.com nothing but the request: it reads which manager release is newest about once a day
registry.npmjs.org nothing but the request: it reads which Claude Code and Codex releases are newest about once a day

When you ask for an engine update in the app, the CLI’s own updater also fetches the new version from wherever that CLI is published. It runs in the same sandbox as the agents and is given no credential. The request names an engine and nothing else, so a paired phone cannot choose what is installed. It is confirmed with Face ID or your passcode, like adding a repository, and who asked is written to the event log.

The relay exists because Apple accepts a notification for the app only from the app’s publisher. It is built so that carrying every notification teaches it nothing: the text is sealed with AES-256-GCM to a key the phone generated and handed to your manager over its own mutually authenticated connection; what a phone shows before opening it is the relay’s own fixed line for the kind (“An agent is waiting for you”), so the relay accepts no text from anyone; and it keeps a daily count per account, not device tokens. push.enabled: false turns it off.

The manager’s service definition deliberately carries no credential. GitHub authentication lives in gh’s own store; the Claude subscription token lives with the process that launches the agents. agentarivm doctor checks that the credential is in place and correctly protected — without ever reading its value.

What it writes to disk, and who can read it

Section titled “What it writes to disk, and who can read it”

Everything Agentarivm creates under your home directory is yours alone — directories 0700, files 0600. That keeps other accounts on the machine out. Keeping out an agent running as you is the sandbox’s job, above:

~/.config/agentarivm/ the config, and the device CA
~/.local/share/agentarivm/ the database, its journal, the cached entitlement
~/.local/run/agentarivm/ the report socket, the readiness signal
~/Library/Logs/Agentarivm/ the manager log
your worktrees and clones checked-out code

The one exception is the release directory itself (versions/), which holds nothing but the runtime and the published build — the same bytes anyone can download.

This matters more than it first appears on macOS, because every local account shares the staff group. A directory that is merely group-readable is readable by every other user on the machine, including a CI runner or a second account you set up for something unrelated.

The permissions are re-applied every time the manager starts, not just when a directory is first created. A version that got this wrong is corrected by upgrading and restarting — there is nothing to run by hand.