Security model

What holds. What does not. How to check.

HQ runs tool-using agents that can execute shell commands. That is the risk, so this page says what holds, what does not, and how to check the parts that ship to your server.

Flat isometric illustration: a vault cube with a round barred door inside a stepped wall, small cube figures passing parcels through one checked gate.

Supported deployment shapes

Local, single user

The default. The web server binds to 127.0.0.1. The machine and its user are trusted. Set web_auth_token on a shared machine.

Private network

HQ stays on loopback behind tailscale serve or a reverse proxy. List the served origins in web_allowed_origins.

Public /mcp

Only through a proxy that exposes /mcp and /health and nothing else, with AGENTHQ_API_KEY set. The claude.ai connector publishes its own set of paths instead (next card). /ws and /api must never be public.

claude.ai connector

Off by default. The public paths are /mcp, /oauth/token, /oauth/revoke and the /.well-known/oauth-* documents; the consent page stays private. You approve each connection as Read only, Tasks or Full. In Full, risky calls (mail, config, terminal panes, commits and similar) wait for your approval, and every call is logged. See docs/CLAUDE_CONNECTOR.md and the connector TLS threat model.

Edge forwarder

hq edge runs on a public machine you control and reaches a device behind NAT. It reads only the TLS server name and never decrypts: TLS ends on the device, and the edge holds no device key or token. Invite-only and off by default. See the edge threat model.

Phone (developer preview)

The Android build runs the mobile profile: no agent shell, since a phone has no sandbox backend, no git and no self-updater. You build the APK yourself. It pairs with another machine by QR code and six typed digits. See the pairing threat model and, for the macOS app, the desktop threat model.

What it protects against

Each line is a control that exists in the code and is described in the repository docs. None of them is a promise that nothing can go wrong.

  • A prompt-injected agent reading your secrets

    Credential files such as ~/.ssh, cloud credentials and the HQ config are refused for every tool, always. Shell commands start with an empty environment plus a short allowlist, and the OS sandbox masks config, env files and private SSH keys.

  • Data leaving after untrusted content

    Once a session reads a web page, mail, a converted document, another agent's mailbox, a pane or a remote MCP result, project secrets and outbound network from bash are denied. Sub-agents share the taint, so delegating does not clear it.

  • Server-side request forgery

    web_fetch refuses loopback, private, link-local and metadata addresses, checks every redirect hop and pins the resolved address so DNS rebinding cannot move it.

  • A hostile web page talking to your local server

    An origin guard refuses cross-origin requests and unknown Host names, which stops DNS rebinding. Tokens travel only in headers, and the chat socket opens with a single-use ticket.

  • A public MCP endpoint without a key

    /mcp refuses every request until a key is configured. Keys have scopes, and the gateway sets the scope itself.

  • Tampered or replayed updates

    Releases and channel pointers are minisign-signed. The updater verifies hashes, rejects unsafe archives and lower sequence numbers, and rolls back on a failed health check. The build side holds no credential for any instance.

  • The agent replacing its own binary

    Self-update tools are off by default. When on, installing needs an owner approval bound to the exact tree and binary hash.

  • The first stranger to message your bot

    Chat relays no longer adopt the first sender as owner. You pair explicitly.

  • Cross-person disclosure in shared chats

    Notes and tasks carry labels, and a shared channel sees only what every verified participant may see. Unknown identities get public content only.

  • Coding agents being clicked through

    HQ never answers a trust dialog for you, and driving a session has mechanical limits that do not depend on the model deciding to stop.

  • A personal agent reaching past its lane

    Hermes Agent and OpenClaw run in the same process sandbox as coding agents, with no HQ token and no MCP wiring. The default open profile walls off HQ itself, other agents' logins and your credential stores, and leaves them the machine's network. The strict profile adds an egress allowlist with no model provider on it until you add one. HQ refuses to give them code work, and their output goes to a person for review (or, with opt-in task verification, to an evidence check), never straight to done.

Known gaps

What it does not protect against

Plain list of limits, taken from the project's own security notes.

  • 01

    The model itself.

    A refusal is a mitigation, not a boundary. The boundaries are the sandbox, the environment allowlist and the governance rules. A prompt injection that gets past those is in scope for a report. A model that merely behaves badly is not.

  • 02

    The bash text checks.

    They catch honest mistakes and cheap tricks. Variables, string building, two-step commands and interpreter code can still get through, and the docs list the shapes. Without a working sandbox, you are relying on them plus the environment allowlist.

  • 03

    The sandbox itself.

    HOME stays writable, so shell rc files can be changed and will run later outside the sandbox. The sandbox shares the host network unless you set network to false, and that cuts off gh and git from bash. On macOS, a bash child can still see the daemon's environment with ps.

  • 04

    Coding agents HQ starts.

    A harness session is a separate program with its own shell. The host starts each agent from a clean environment and runs it in a process sandbox, but whatever you hand an agent (profile env values, wrapper scripts) is readable by a prompt-injected session. Keep secrets out of profile env.

  • 05

    Personal agents HQ runs.

    Each keeps its own model provider key in its own state folder, which it can read, so a prompt-injected session can use it. HQ cannot cap their token spend. Give them a key with a spending limit set at the provider.

  • 06

    The handoff key.

    It is equivalent to code execution on every host you paired. Bound it with agent_host.handoff_cwd_allow, and give it only to callers you would give a shell.

  • 07

    An attacker who is already you.

    Anyone who controls the host user, the vault directory or the config file is out of scope. On loopback with no token set, any process on the machine can call the API without an Origin header, so set a token on shared machines.

  • 08

    A stolen signing key, or a stale pointer.

    Both are listed as not mitigated by the updater. A compromised service user can also fake a healthy /health answer to defeat the automatic rollback. Locally built self-update binaries are not signed.

  • 09

    Content already in the prompt.

    Vault text the context engine injects, and earlier turns in a thread, are not tracked as taint. Only tool results in the current session are.

  • 10

    Independent review.

    HQ has not had an outside security audit. It is version 0.9.x. Treat it that way.

Verifying a signed release

The updater does this for you, in process, on every update. You can repeat it by hand. The trust chain is the minisign signature on manifest.json, then the SHA-256 of each artifact in that signed manifest. SHA256SUMS in the release is unsigned and for humans, so do not rely on it alone.

Get the official public key from the repository file release/minisign.pub, not from a page that could be altered, and compare. Its key id is 51FDC63C4F4E8BD2 and its key line begins RWTSi05PPMb9. Install the minisign tool and jq first.

Trust chain

  1. Download the manifest, its signature, the binary archive and the key.

    Download
    V=<version>   # for example 0.9.1, without the leading v
    B=https://github.com/CalvinMagezi/hq/releases/download/v$V
    curl -fsSLO "$B/manifest.json"
    curl -fsSLO "$B/manifest.json.minisig"
    curl -fsSLO "$B/hq-$V-linux-x86_64.tar.gz"
    curl -fsSLO "https://github.com/CalvinMagezi/hq/raw/main/release/minisign.pub"
  2. Verify the manifest signature. It must report a good signature.

    Signature
    minisign -Vm manifest.json -p minisign.pub
  3. Check each downloaded artifact against the hash in the signed manifest.

    Hashes
    jq -r '.artifacts[] | "\(.sha256)  \(.name)"' manifest.json | sha256sum -c --ignore-missing
  4. On an instance, --dry-run repeats every check, including the channel pointer, and changes nothing.

    Dry run
    hq update --apply --dry-run

Channel pointers (channel-stable.json and channel-main.json) have their own .minisig. Only prehashed signatures are accepted, which is the default of minisign -S. Format and trust model: docs/UPDATE_SYSTEM.md.

Privately, never in a public issue

Reporting a vulnerability

Report privately, not in a public issue. Use GitHub's private vulnerability reporting on the repository. If that is unavailable, open a public issue that says only that you have a security report and asks for a private channel, with no details.

Include what you found, how to reproduce it, the affected version and the impact you expect. Expect an acknowledgement within 7 days, and please allow 90 days or until a fix ships before disclosing. Do not include real credentials or other people's vault contents. Only the latest release and the latest commit on main are supported.

The full policy, including what is in and out of scope, is in SECURITY.md.