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.
Security model
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.
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.
HQ stays on loopback behind tailscale serve or a reverse proxy. List the served origins in web_allowed_origins.
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.
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.
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.
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.
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.
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.
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.
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.
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.
/mcp refuses every request until a key is configured. Keys have scopes, and the gateway sets the scope itself.
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.
Self-update tools are off by default. When on, installing needs an owner approval bound to the exact tree and binary hash.
Chat relays no longer adopt the first sender as owner. You pair explicitly.
Notes and tasks carry labels, and a shared channel sees only what every verified participant may see. Unknown identities get public content only.
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.
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.
Plain list of limits, taken from the project's own security notes.
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.
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.
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.
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.
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.
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.
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.
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.
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.
HQ has not had an outside security audit. It is version 0.9.x. Treat it that way.
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
minisign.pubFrom the repomanifest.json.minisigGood signaturemanifest.jsonSHA-256 per artifacthq-<V>-linux-x86_64.tar.gzHash matchesDownload the manifest, its signature, the binary archive and the key.
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"
Verify the manifest signature. It must report a good signature.
minisign -Vm manifest.json -p minisign.pub
Check each downloaded artifact against the hash in the signed manifest.
jq -r '.artifacts[] | "\(.sha256) \(.name)"' manifest.json | sha256sum -c --ignore-missing
On an instance, --dry-run repeats every check, including the channel pointer, and changes nothing.
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
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.