Skip to content
Pagr

Reference

Security model

The bridge is the only piece of Pagr that runs on your machine, so it is the piece an attacker would most like to control. This is what it assumes, what it enforces, and where to look if you want to check.

Threat model#

The assumptions are deliberately pessimistic: the network is hostile, the Pagr cloud could be compromised or coerced, and another local user or process might poke at the daemon. The design goal follows from that — even a fully compromised cloud cannot turn the bridge into a remote shell.

Everything below is enforced in code you can read. The bridge is open source under Apache-2.0.

What the bridge can be asked to do#

Only the nine commands defined in the protocol schema:

Command effects
CommandEffect on your machine
device.probe, project.list, agent.get_statusRead-only status
project.removeForget a project id — never deletes files
agent.start_sessionStart Claude Code or Codex in a project you registered locally, with an instruction and up to four images
agent.send_instructionSend follow-up text to an existing session
agent.stop_sessionInterrupt a session
agent.respond_to_approvalAnswer a permission prompt, bound to the exact preview you saw
settings.sync_public_policyUpdate the approval timeout and the Tier A auto-approve flag

There is no shell.exec, fs.read, fs.write, process.spawn, or “run this binary”. The cloud cannot send a filesystem path: project references are opaque ids that only resolve against your local registry, and a path where an id is expected fails schema validation before anything else runs.

Command authentication#

Eight checks, in order. The first failure rejects the command with a typed command.ack and nothing executes: schema, known signing key, Ed25519 signature over the canonical body, device binding, expiry, replay, idempotency, and local existence of whatever the command names. The full table is in how the bridge works.

A compromised gateway that lacks the server signing key can therefore do nothing; a stolen signing key still cannot target a different device, replay old commands, or reach unregistered directories.

Device identity#

  • Ed25519 keypair generated locally. Only the public key is sent, and only at pairing.
  • The private key lives in the macOS Keychain under service dev.pagr.bridge, with a /usr/bin/security fallback.
  • Gateway authentication is challenge/response — the bridge signs the device id, a dot and the nonce. There is no long-lived bearer token.
  • Revoking the device in the dashboard removes the public key server-side; the bridge is refused at its next connection.

No inbound network surface#

The bridge never listens on a TCP port. The gateway connection is an outbound WebSocket over TLS with exponential backoff. The only local endpoint is a Unix-domain socket at ~/.pagr/run/daemon.sock:

  • created mode 0600 inside a 0700 directory;
  • a stale socket is unlinked on start only if it is genuinely a socket owned by the current uid;
  • ownership and mode are re-verified after bind;
  • requests are newline-delimited JSON with a 1 MiB line cap, and unknown methods are refused.

Approval binding#

Each pending approval is bound to the session id, the provider's request id, and the sha256 of the preview. The decision must echo all three; a mismatch is rejected. Entries are single-use and expire after the policy timeout — 600 seconds by default — at which point the provider is told deny. When the provider resolves a request on its own, because you answered in the terminal, the bridge records that and does not answer twice.

How a request is classified into Tier A, B or C is in approvals.

Filesystem containment#

  • Registration requires an existing directory that is a git repository — or --allow-non-git — resolved with realpath.
  • Refused: /, /System, /private/etc, /etc, /usr, /bin, /sbin, /Library, your home directory itself, and anything under ~/.pagr.
  • Containment realpaths the candidate, following symlinks, and requires the result to stay under the project root.

Attachments#

Downloads use a 15-second timeout, are capped at the declared size, must match the declared sha256, and must sniff as PNG, JPEG, HEIC or WebP by magic bytes — the URL's extension and its Content-Type are ignored. Files are written 0600 and deleted in a finally block after the agent call. See screenshots and attachments.

Supply chain#

Runtime dependencies are ws, zod and @napi-rs/keyring. Child processes are spawned with argument arrays, never through a shell. There is no auto-update mechanism in the bridge repository.

There is also no telemetry — no analytics, no crash reports, no usage metrics. The only network call sites in the bridge core are the gateway transport, the two pairing endpoints, and attachment downloads. You can verify that by grepping for fetch( and new WebSocket.

Reporting a vulnerability#

Email security@pagr.dev with a description and a reproduction. Please do not open a public issue for anything exploitable. We aim to acknowledge within two business days and to ship a fix — with credit, if you want it — before public disclosure.

security@pagr.dev