Using Hazmat
June 29, 2026 · View on GitHub
Hazmat runs AI agents on your Mac with full permissions — inside containment. Every session prints a contract telling you exactly what the agent can do, which mode was selected, and why.
Picking which agent to install? docs/harnesses.md is the per-harness setup matrix — tested versions, auth paths, and verification commands for claude, codex, opencode, antigravity, experimental hermes, qwen, cursor-agent, and pi.
Verifying a fresh install or a release candidate? docs/manual-testing.md is the human-driven checklist with preconditions, per-harness flows, regression scenarios, and recovery moves.
Quick Start
Install:
# Homebrew
brew install dredozubov/tap/hazmat
# Or GitHub releases (downloads, verifies checksum, installs)
curl -fsSL https://raw.githubusercontent.com/dredozubov/hazmat/master/scripts/install.sh | bash
The release installer targets the current host by default and currently
publishes only darwin/arm64 and darwin/amd64 artifacts. Linux is
compile-only until its provider lanes pass their separate gates:
linux-current-user is plan-only, and linux-agent-user is setup-required.
macOS native execution defaults to macos-agent-user. macos-current-user is
an exec-only experimental same-UID Seatbelt lane selected with
--provider=macos-current-user and gated by
HAZMAT_EXPERIMENTAL_MACOS_CURRENT_USER=1; it is not the same boundary as the
dedicated agent user. See Runtime provider status
for the status vocabulary.
Then two commands:
hazmat init --bootstrap-agent claude # one-time setup (~10 min, needs sudo)
hazmat claude # launch Claude Code in containment
That's it for the Claude path. If you use another supported harness, substitute the harness name:
hazmat init --bootstrap-agent codex
hazmat codex
hazmat init --bootstrap-agent qwen
hazmat qwen
hazmat init --bootstrap-agent cursor-agent
hazmat cursor-agent -- --version
hazmat init --bootstrap-agent pi
hazmat pi -- --version
init creates a contained environment and lets you choose whether to bootstrap
Claude Code, Codex, OpenCode, Antigravity, Hermes, Qwen, Cursor Agent, Pi, or skip agent
installation for now. When you bootstrap an agent during init, Hazmat can also
ask for reusable provider API keys and git credentials. Import prompts are
offered only for harnesses with a supported host-profile import path; Hermes,
Qwen, Cursor Agent, and Pi do not import host profile state in v1.
flowchart LR
subgraph once ["One-time setup (hazmat init)"]
direction TB
I1[Create agent user] --> I2[Set up workspace ACLs]
I2 --> I3[Init snapshot repo]
I3 --> I4[Install firewall + DNS blocklist]
I4 --> I5["Optional: bootstrap a supported harness"]
I5 --> I6["Optional: configure provider keys + git creds"]
end
subgraph daily ["Every session (hazmat <harness>)"]
direction TB
D1[Snapshot project] --> D2[Generate seatbelt policy]
D2 --> D3[Resolve integrations and path extensions]
D3 --> D4[Print session contract]
D4 --> D5[Launch agent in containment]
end
once --> daily
style once fill:#f5f5ff,stroke:#33a,color:#000
style daily fill:#f5fff5,stroke:#3a3,color:#000
What hazmat init Does
When you run hazmat init, it:
- Creates a hidden
agentmacOS user (separate from yours) - Adds the host-side access needed for contained sessions to reach the selected project directories
- Initializes the local Kopia repository for automatic pre-session snapshots
- Installs a firewall that blocks the agent from SMTP, IRC, FTP, Tor, and other exfiltration protocols
- Adds a DNS blocklist for tunnel and paste services (ngrok, pastebin, etc.)
- Optionally bootstraps a supported AI coding agent for the agent user
- If you choose a provider-backed harness, offers to configure reusable provider API keys and git credentials
- If the harness supports import, can optionally import portable basics such as sign-in state, commands, and skills
Everything is interactive — it explains each step and asks for confirmation. To preview without making changes:
hazmat init --dry-run
The Session Contract
Every session starts with a plain-language summary of what the agent can and can't do:
hazmat: session
Mode: Native containment
Why this mode: using native containment by default (Docker routing: none)
Project (read-write): /Users/dr/workspace/my-app
Integrations: go
Host changes: project ACL repair
Auto read-only: /Users/dr/go/pkg/mod
Read-only extensions: /Users/dr/reference-docs
Read-write extensions: /Users/dr/.venvs/my-app
Service access: none
Credential env grants: none
Pre-session snapshot: on
Snapshot excludes: vendor/
Each line maps to a concrete boundary:
- Mode — Native containment (kernel sandbox + user isolation) or Docker Sandbox (private Docker daemon in an isolated runtime)
- Why this mode — what triggered the mode selection (
--docker=sandbox,--docker=auto, project config, or the default native mode) - Project (read-write) — the only directory the agent can modify
- Integrations — active stack integrations and what they add automatically
- Host changes — persistent host-side mutations Hazmat may apply before launch, such as bounded project ACL startup repair, agent Git safe-directory trust, or a bounded toolchain permission fix. Permission-repair classes are modeled in TLA+; non-permission host changes are governed by tests and documentation.
- Auto read-only — read-only directories that Hazmat resolved on your behalf
- Read-only extensions — explicit additional read-only directories from
-Ror config - Read-write extensions — explicit additional writable directories from
-Wor config - Service access — external services the agent can authenticate to
- Credential env grants — redacted environment-delivered credentials granted explicitly for this session
- Pre-session snapshot — whether a rollback point was created
- Snapshot excludes — patterns skipped by the snapshot (often from integrations)
Preview any session without running it:
hazmat explain # preview current project
hazmat explain --json # machine-readable preview for automation
hazmat explain --docker=sandbox # preview Docker Sandbox mode
hazmat explain --docker=auto # preview marker-based Docker routing
hazmat explain --for qwen # preview a non-Claude harness
hazmat explain --integration node # preview with an integration
hazmat explain --github # preview explicit GitHub API access
# Apple Container backend preview (live exec is gated):
hazmat explain --backend=apple-container --image ghcr.io/example/hazmat-codex:latest --for codex
The --backend=apple-container preview compiles the session contract into an
Apple Container launch spec — image, deterministic container name, non-root
guest identity, bind mounts, network policy, and cleanup obligations — and
lists the remaining capability gaps. Preview is plan-only; live execution is
available only through the gated hazmat exec --backend=apple-container path
below. The launch boundary is proved in tla/MC_AppleContainerLaunch.
An experimental executable path exists for hazmat exec only, behind an
explicit gate:
HAZMAT_EXPERIMENTAL_APPLE_CONTAINER=1 \
hazmat exec --backend=apple-container --image alpine:latest -- uname -a
Be clear about what this backend is: Linux VM-per-session execution with
Hazmat-planned host mounts. Host file IO occurs as the invoking macOS
user (the container CLI is per-user-session and cannot run as the agent
user). Host account isolation is not provided by this backend; use
native containment for that. The strict mount plan is the boundary: no home
mount, no credential paths or parents, no sockets, no SSH forwarding, no
host env inheritance, always a non-root guest user. Only --network default is supported (honestly reported as outbound-allowed; host services
bound to 0.0.0.0 are reachable from the guest VM network). Requires macOS
26 on Apple silicon with apple/container >= 1.0.0 running.
hazmat explain previews these changes but does not apply them. A real session
may execute the listed host mutations before launch if they are still needed at
that point. The verified TLA+ model covers the permission-repair subset of that
preview-vs-launch split and the current non-reverting rollback contract for
those repairs; non-permission host changes are covered by tests and docs.
hazmat explain --json emits the same prepared session state in a stable
machine-readable form, including suggested integrations, active integrations,
resolved integration sources and details, planned host changes,
read-only access, snapshot excludes, and routing notes.
Daily Usage
cd ~/workspace/my-project
hazmat claude
hazmat codex
hazmat opencode
hazmat antigravity
hazmat hermes -- --version
hazmat qwen
This generates a per-session security policy, switches to the agent user, and launches the agent inside containment. When you exit, the session is cleaned up.
Harness code can be inspected, refreshed, or removed without deleting auth and profile state:
hazmat harness status
hazmat harness status codex
hazmat harness update codex
hazmat harness uninstall codex --dry-run
The older hazmat bootstrap <harness> commands remain compatible aliases for
install/update. hazmat harness uninstall <harness> removes only declared
Hazmat-owned code artifacts and selected Hazmat metadata by default; auth,
profile roots, sessions, provider keys, and imported basics are preserved.
Denying Network Egress for One Native Session
Native sessions allow outbound network by default so harnesses can reach their
model provider, package registries, and other explicit services. For offline
review harnesses, pass --network none to remove outbound network authority
from that one Seatbelt policy:
hazmat claude --network none --metadata-json -p "review this packet offline"
hazmat codex --network none --metadata-json exec "review this packet offline"
hazmat opencode --network none --metadata-json run "review this packet offline"
hazmat antigravity --network none --metadata-json -p "review this packet offline"
hazmat hermes --network none --metadata-json -- --version
hazmat qwen --network none --metadata-json -p "review this packet offline"
hazmat exec --network none --metadata-json -- /bin/zsh -lc 'make test'
hazmat explain --network none --json
--network none is native-only. It omits the network-outbound Seatbelt grant
and the DNS resolver mach lookup, so outbound IPv4, outbound IPv6, and DNS
resolution fail closed for the sandboxed process tree. Hazmat does not install
or remove per-session pf rules for this mode; concurrent default sessions keep
their normal network behavior, and there is no network cleanup artifact after
normal exit, timeout, or signal.
Threat boundary: this mode blocks network egress, including provider API calls, package downloads, Git remotes, DNS, and loopback outbound dials. It is not a VM boundary and does not make already-visible local files secret from the harness. Local inbound listener support remains in the profile for tools that bind preview servers, but it is not an outbound path.
--metadata-json emits one compact JSON line to stderr after the native helper
has applied the Seatbelt policy. The child harness stdout is left untouched, so
non-interactive callers can still capture structured stdout while verifying:
{"kind":"hazmat.session","network_policy":{"requested":"none","effective":"none","enforced":true}}
Giving the Agent Access to Other Directories
By default, the agent can only write to the project directory (your current directory). To let it read or write other directories explicitly:
hazmat claude -R ~/workspace # read all of ~/workspace
hazmat claude -R ~/code/lib -R ~/docs # cherry-pick specific dirs
hazmat claude -W ~/.venvs/my-app # add another writable root
hazmat config access add -C ~/workspace/my-project --read ~/docs --write ~/.venvs/my-app
Read directories are strictly read-only. Write directories are explicit extensions to the writable contract and show up separately in the session summary.
Session Integrations
Integrations let you carry stack-specific ergonomics into a session without weakening Hazmat's trust boundaries:
hazmat integration list
hazmat integration show node
hazmat claude --integration node
hazmat claude --integration python-uv
hazmat config set integrations.pin "~/workspace/my-project:node,go"
Today integrations can:
- add auto-resolved read-only toolchain or cache directories
- add snapshot excludes for reproducible build artifacts
- pass through a small safe set of environment selectors such as
GOPATHorVIRTUAL_ENV
They do not widen write access, expose blocked credentials, or change firewall
policy. Explicit extra writable scope is handled separately through -W or
hazmat config access, not through integrations.
Built-in integrations may also plan narrowly-scoped host permission repairs for
known local toolchains when the current host permissions would otherwise block
the agent user. These changes are shown under Host changes before launch,
are never applied by hazmat explain, and the permission-repair subset shares
the same TLA+ state-machine coverage as the other modeled session mutation
classes.
Repos can still ship a .hazmat/integrations.yaml listing recommended integrations.
On first use, hazmat prompts once for approval; after that, the approved
integrations activate automatically until the file changes. Write your own
integration manifest in ~/.hazmat/integrations/ for environments that
built-ins do not cover. Full reference: integrations.md.
For mixed-stack repos, prefer declaring the full set explicitly. Example:
integrations:
- python-uv
- node
- tla-java
Repo-local Git Hooks
Hazmat also supports a narrow repo-local Git hook flow for the common cases:
pre-commit, commit-msg, and pre-push.
The repo declares intent in tracked files under .hazmat/hooks/. The host owns
activation. Hazmat only runs hook code after approval, and it runs the approved
snapshot bytes from host-owned storage rather than the live repo copy. Hook
bundles can also include tracked auxiliary files under .hazmat/hooks/ such as
scanner config; those files are hashed, approved, and snapshotted alongside the
declared hook entrypoints.
hazmat hooks status
hazmat hooks review
hazmat hooks install
hazmat hooks install --chain-existing # preserve an existing local core.hooksPath owner
hazmat hooks install --replace # take over an existing local core.hooksPath owner
hazmat hooks uninstall
On the next session launch, Hazmat can also surface the same approval/install flow automatically. The prompt is manifest-driven: hook type, purpose, interpreter, and required binaries, plus a calm drift summary when the repo bundle changes.
V1 scope is intentionally narrow:
- repo-local hooks only
pre-commit,commit-msg,pre-pushonly- explicit install / uninstall through Hazmat
- refusal when another local
core.hooksPathowner already exists unless you passhazmat hooks install --chain-existingto preserve it orhazmat hooks install --replaceto take over
--chain-existing is the coexistence path for repos where another tool already
owns a repo-relative hooks path such as .beads/hooks. Hazmat leaves
core.hooksPath pointing there, inserts a Hazmat-managed block into the
declared hook files, records the resulting file hashes in host-owned approval
state, and refuses later if that block drifts. The other tool's hook body still
runs as that tool's responsibility; Hazmat's approval only covers the
Hazmat-declared snapshot.
V1 does not support global hooks, init.templateDir, package-manager
auto-install, post-* hooks, or server-side hooks.
If that flow feels stricter than normal Git hooks, see git-hooks.md for the threat model, attack vectors, and the specific risks Hazmat is trying to close.
Docker Projects
Hazmat treats Docker routing as an explicit daemon-boundary choice, not just "does this repo have Docker files?"
- By default, sessions use native containment with Docker disabled, even when Docker files are present.
- Use
--docker=sandboxto force Docker Sandbox mode for a private-daemon workflow. - Use
--docker=autoorhazmat config docker autowhen you want Hazmat to inspect Docker markers and route private-daemon fits automatically. - If auto mode sees shared host daemon signals (for example external Docker networks or Traefik Docker labels), Hazmat stops and asks you to use native code-only mode or move the workflow to Tier 4.
hazmat claude # native code-only session
hazmat claude --docker=sandbox # force Docker Sandbox mode
hazmat claude --docker=auto # marker-based Docker routing
hazmat config docker auto -C ~/workspace/my-project
Docker Sandbox sessions are available through every harness entrypoint:
hazmat claude, hazmat codex, hazmat opencode, hazmat antigravity,
hazmat hermes, hazmat qwen, hazmat shell, and hazmat exec.
--docker=auto keeps the default native path for code-only repos and routes
Docker-heavy private-daemon fits into the matching harness automatically.
If .devcontainer/ is the only Docker-related directory, Hazmat stays in
native containment unless the devcontainer.json positively indicates Docker
is needed (e.g., it contains image, dockerFile, or dockerComposeFile).
Native code-only mode is the default for editing against externally managed
local services. Docker commands still fail inside the session. If the agent must
restart containers, inspect logs, run docker exec, or debug the live Docker
topology, Tier 4 is the right fit.
For setup details, network policy, and Compose hardening guidance, see tier3-docker-sandboxes.md. For shared-daemon projects and the code-only fallback, see shared-daemon-projects.md.
Specifying a Different Project Directory
hazmat claude -C ~/workspace/other-project
Running Commands With Flags
hazmat exec forwards the command after Hazmat parses its own flags. When the
forwarded command has flags of its own, insert -- before it:
hazmat exec -- make test
hazmat exec -- /bin/zsh -lc 'uv run pytest -q'
hazmat exec --audit-install -- npm install
hazmat exec --docker=none -C ~/workspace/app -- /bin/zsh -lc 'cd frontend && npm run build'
hazmat exec --audit-install is native-only and observational. Hazmat samples
established TCP endpoints for the agent user from the host side while the
command runs, then prints a post-run egress report. It does not block the
package manager or replace --network none.
Resuming a Conversation Inside Native Containment
When you start a conversation as yourself (claude) and later want to continue it inside native containment, --resume and --continue work seamlessly:
# Start a conversation as yourself (no containment)
cd ~/workspace/my-project
claude
# Later, resume that same conversation inside containment
hazmat claude --resume # interactive picker — shows your sessions
hazmat claude --continue # resume the most recent session
hazmat claude --resume <session-id> # resume a specific session by ID
How it works: Hazmat detects --resume or --continue in the forwarded flags and copies the matching host Claude session transcripts into the agent user's local Claude session directory before launch.
hazmat claude --resumecopies the project's available sessions so Claude can show its picker UIhazmat claude --continuecopies only the latest sessionhazmat claude --resume <session-id>copies one specific session- Existing agent-local files are not overwritten, so contained continuations stay independent once they diverge
Security note: The sandbox does not get direct access to your host ~/.claude/projects/ directory. Hazmat stages copies into the agent-owned Claude store instead.
Current limitation: Docker Sandbox mode uses sandbox-local Claude history. Host transcript sync is not applied there yet.
Continuing a Hazmat Session Outside the Sandbox
When a conversation started inside containment and you want to continue it as your normal user, export the hazmat session into your host Claude session store and then resume it:
# Continue the latest hazmat Claude session for the current project
claude --resume "$(hazmat export claude session)" --fork-session
# Continue a specific hazmat session
claude --resume "$(hazmat export claude session <session-id>)" --fork-session
# Export from a different project directory
claude --resume "$(hazmat export claude session -C ~/workspace/other-project)" --fork-session
What hazmat export claude session does:
- Defaults to the latest hazmat Claude session for the current project
- Accepts an optional session ID to export a specific session
- Copies the transcript and session sidecar directory from the agent user's
~/.claude/projects/... - Rewrites portable JSON/JSONL metadata so references to the agent project/session directory point at the installed host-side copy
- Omits opaque Workflow/subagent sidecar files that still contain agent-only paths after export
- Updates your host Claude
sessions-index.json - Prints the Claude resume ID on stdout for scripting
Workflow/subagent caches are best-effort. Portable JSON/JSONL metadata is kept
and rebased, but opaque cache files that would send host Claude back to
/Users/agent/.claude/projects/... are deliberately dropped instead of copied
with stale paths. That means a host-side resume should not try to read
inaccessible agent-home Workflow artifacts, but Claude may rerun volatile
Workflow steps whose cache files were not portable.
--fork-session is recommended so your host-side continuation cleanly diverges from the contained hazmat session. The export is a point-in-time handoff, not a live sync. If the hazmat session advances later, run the export again before resuming.
Running Other Commands in Containment
hazmat shell # interactive shell as the agent user
hazmat exec npm install # run a single command
hazmat exec --audit-install -- npm install
hazmat exec -C ~/workspace/proj npm test
hazmat opencode -C ~/workspace/proj
Checking Status
hazmat # shows setup progress checklist
hazmat status # setup progress checklist
hazmat check # read-only health and repairability report
hazmat doctor --fix # apply executable approved typed repairs
hazmat doctor --dry-run # explicit non-mutating repair plan preview
hazmat status --full # setup progress plus full live validation
hazmat check --full # sudo-adjacent launch-helper, helper-backed, backup, and cloud live validation
hazmat status is the lightweight setup progress checklist. It shows where the
host is in the setup flow and what the next setup or repair command should be.
hazmat status --full keeps that progress checklist and then runs the same
approval-gated full validation as hazmat check --full.
hazmat check validates the current local Hazmat install and containment
behavior without mutating host state. It reports typed findings, repairability,
and the next command to run. hazmat doctor --dry-run runs the same diagnostics
under repair-oriented command naming and shows the typed repair plan without
applying it. Plain hazmat doctor remains compatible and plan-only, but docs
and recommendations use --dry-run when preview behavior matters. Applying
repairs requires hazmat doctor --fix.
Neither command is the full repo test suite. For lifecycle e2e, self-hosting,
repo-matrix, VM-backed verification, and CI mapping, see testing.md.
hazmat check --full adds sudo-adjacent launch-helper validation plus
helper-backed, backup, and cloud live validation. It can send external traffic
and is sudo-adjacent for agent workflows, so automation should ask for explicit
approval before running it.
Diagnostic and Repair Contract
hazmat check is always read-only. It does not accept --fix, does not repair
Homebrew permissions, does not edit setup files, and does not use warning text to
invent shell recipes. Its job is to report health, repairability, and evidence.
hazmat doctor --dry-run is the explicit repair-planning command. The plan is
built from Hazmat-owned finding and repair-action IDs, not from strings printed
by checks or from repo-controlled metadata. --json emits the same typed plan
for automation, including authority, consent model, proof lanes, rollback
boundary, verification target, and receipt ID.
Terminal output includes a compact repair-plan summary before the detailed item
list: executable repairs, manual items, skipped or informational items, applied
repairs, failed verifications, and remaining items. JSON reports expose the same
counts under repair_plan.summary, so automation does not need to infer the
repair outcome by scanning every item array.
JSON reports also include repair_plan.next_steps. These entries are ordered by
the intended next action and carry explicit command, mutating, and
requires_approval fields where a command applies. requires_approval is not
limited to mutating repairs: sudo-adjacent verification such as hazmat check --full can be non-mutating and still require exact-command approval. Read-only
hazmat check reports put the approved repair path before the preview path when
executable repairs exist, while post-init verification points to hazmat doctor --fix instead of retrying hazmat init. If a plan has remaining findings but
no executable Hazmat repairs, next_steps uses inspect-remaining-items with
no mutating command, and repair_plan.execution.examples omits the mutating fix
command so automation does not offer a repair path that cannot change the
machine.
hazmat doctor --fix is the only diagnostics entrypoint allowed to apply typed
repairs. Interactive runs may ask for consent before applying a plan.
Non-interactive mutation requires both --fix and --yes. Hazmat will not run
arbitrary shell commands from findings, project files, or recommendation text.
Repairs that are not wired to the typed executor stay manual, optional,
unsupported, or informational.
Global --dry-run always wins over repair execution. For example,
hazmat doctor --fix --dry-run still produces a non-mutating preview and does
not dispatch typed repair backends.
Repair receipts name what Hazmat changed and which verification passed after the change. A failed verification is not turned back into a generic "run init" recommendation; the report keeps the attempted action, evidence, and next classification so the same loop is visible and testable.
hazmat init remains the first-time baseline setup command. It owns creating the
agent account, shared group, network policy, launch helper, shell defaults, and
other setup resources. It should converge those resources or report a specific
blocker; it is not the generic answer to every check finding. After init,
use hazmat doctor --fix to apply executable post-init repairs, hazmat doctor --dry-run to preview the typed plan, or hazmat check when you only want a
read-only health report.
Historical project ACL backfill is explicit because it can be proportional to repo size. Normal launches repair only a bounded shallow ACL set. To preview or run the full recursive project repair:
hazmat repair project-acl-backfill --dry-run -C ~/workspace/my-app
hazmat repair project-acl-backfill --yes -C ~/workspace/my-app
The backfill command does not use sudo. It applies the collaborative project ACL to non-symlink directories and regular files under the selected project, and it remains outside automatic launch planning.
hazmat rollback removes Hazmat-owned setup state. It does not promise to erase
user data, project files, host-owned credential stores, or session-time
permission repairs that the verified session-repair contract intentionally
preserves. Rollback output should call out preserved residue rather than
implying the machine is byte-for-byte restored.
Backup and Restore
Local project snapshots
Hazmat automatically snapshots the current project directory before every session:
hazmat snapshots
hazmat diff
hazmat restore
hazmat restore --session=2
These snapshots cover only the selected project directory, not the entire
workspace and not the extra read-only directories you pass via -R.
Default excludes live in hazmat config, and integrations can add stack-specific
snapshot excludes such as node_modules/ or target/ for the active session.
Cloud backup (encrypted, incremental)
hazmat init cloud # one-time: store S3 endpoint and credentials
hazmat backup --cloud # back up the current project
hazmat backup --cloud -C ~/workspace/my-app
hazmat restore --cloud # restore latest cloud snapshot for current project
hazmat restore --cloud -C ~/workspace/my-app
Cloud backup keeps endpoint and bucket in ~/.hazmat/config.yaml.
Credential material is host-owned under ~/.hazmat/secrets/cloud/; legacy
backup.cloud.access_key, backup.cloud.recovery_key/password, and
~/.hazmat/cloud-credentials entries migrate there automatically.
Configuring cloud credentials does not upload a snapshot or initialize visible
bucket contents. The provider UI can show 0 byte until hazmat backup --cloud
successfully completes for at least one project.
Updating Credentials
hazmat config agent # re-enter native-session API keys, git name/email
hazmat config github # store a GitHub API token for explicit --github sessions
GH_TOKEN=... hazmat config github --token-from-env
hazmat config agent stores native-session provider API keys under
~/.hazmat/secrets/providers/* and injects them only into the matching native
harness. It does not persist API-key exports in /Users/agent/.zshrc.
hazmat config github stores the token under
~/.hazmat/secrets/github/token. Launch commands only expose it when you pass
--github, where it appears in the session as GH_TOKEN and as a redacted
github.api-token grant in the contract and hazmat explain --json. Repo
recommendations and integrations cannot activate it, and ambient
GH_TOKEN/GITHUB_TOKEN passthrough remains rejected.
--github gives the whole harness process GitHub API authority. A write-scoped
token can let the agent create refs, push branches through local tooling, open
or update PRs, edit issues, or otherwise change remote repository state. Use a
least-scoped token and omit --github when the session must not be able to
self-push or alter the review path.
Git HTTPS credentials are brokered per native session. Legacy agent-side
/Users/agent/.config/git/credentials entries migrate into
~/.hazmat/secrets/git-https/credentials on session launch, and hazmat check
reports any old helper or credential-store residue without printing tokens.
For the full registry inventory, run hazmat check and inspect the credential
section. It lists each surface by redacted registry ID, storage backend,
delivery mode, and legacy residue status.
Managed Git SSH
A single-key project config (explicit host required):
hazmat config ssh list-keys
hazmat config ssh add -C ~/workspace/my-project \
--name default --host github.com ~/.ssh/id_ed25519
hazmat config ssh test -C ~/workspace/my-project --host github.com
hazmat config ssh unset -C ~/workspace/my-project
Multi-key per project — one key for GitHub, a separate key for a remote server:
hazmat config ssh add -C ~/workspace/my-project \
--name github --host github.com ~/.ssh/id_ed25519
hazmat config ssh add -C ~/workspace/my-project \
--name prod --host prod.example.com --host '*.prod.example.com' ~/.ssh/prod_key
hazmat config ssh test -C ~/workspace/my-project --host prod.example.com
hazmat config ssh remove -C ~/workspace/my-project --name prod
Every destination host resolves to exactly one configured key. Two keys
whose host lists overlap are rejected at config-save time. Every inline
key must declare at least one --host — the legacy any-host fallback
has been retired. Profile-referencing keys still inherit default_hosts
from the profile when they declare no hosts of their own.
This is an explicit per-project capability for Git transport only. Hazmat
resolves every key to a typed credential reference before launch. External
keys and profile keys remain explicit host-file references; provisioned
inventory keys live under ~/.hazmat/secrets/git-ssh/provisioned/<name>/.
Hazmat routes Git through a session-scoped transport broker. The broker runs
outside the contained session, selects the matching key by destination host,
preserves supported OpenSSH routing such as ProxyJump, and performs only Git
transport commands. The session receives GIT_SSH_COMMAND, not a readable
private key or reusable ssh-agent socket.
Docker Sandbox sessions currently fail closed when this capability is enabled.
The safe parity path is a container-side adapter for the same host-side broker;
Hazmat must not copy private keys into the sandbox or expose an ssh-agent
socket to regain Git SSH support.
Reusable SSH profiles
Define one SSH identity that many projects share. Each profile carries
the private key, an optional known_hosts override, and an optional
default_hosts list that referring projects inherit unless they override:
hazmat config ssh profile add github ~/.ssh/keys/github/id_ed25519 \
--default-host github.com --description "personal github"
hazmat config ssh profile add prod ~/.ssh/keys/prod/id_ed25519 \
--default-host prod.example.com --default-host '*.prod.example.com'
# Attach a profile to a project. Inherits default_hosts:
hazmat config ssh add -C ~/workspace/my-project --name work --profile github
# Or override default_hosts for this project only:
hazmat config ssh add -C ~/workspace/my-project \
--name enterprise --profile github --host enterprise.internal
hazmat config ssh profile list
hazmat config ssh profile show github
hazmat config ssh profile rename github personal-github
hazmat config ssh profile remove personal-github --force # detaches referrers
A profile reference that points to an undefined profile is rejected at config load, not at session launch. Two project keys may safely reference the same profile; the broker still routes each destination host to exactly one configured project key.
For guidance on how to choose and scope keys for GitHub, remote servers,
deploy keys, machine users, SSH certificates, and per-target known_hosts
layouts, see ssh-key-hygiene.md.
hazmat config ssh test is a host-side validation helper. It uses the selected
Hazmat key and known_hosts, but it also honors the host user's real OpenSSH
config for routing, so aliases and jump-host flows from ~/.ssh/config
continue to work during the test. This includes common directives such as
Host, HostName, User, Port, Include, and ProxyJump.
Session-time alias-based Git remotes are still a separate limitation. General SSH shells remain unsupported.
Importing Portable Harness Basics
hazmat config import claude
hazmat config import codex
hazmat config import opencode
hazmat config import claude --dry-run # preview
hazmat config import opencode --overwrite # resolve conflicts by replacing
hazmat config import codex --skip-existing
Hazmat treats imports as curated basics, not full profile migrations. Supported
imports store sign-in state in the host-owned secret store, copy a narrow set of
portable prompt/config assets, and import git identity when available. File-backed
auth is materialized into /Users/agent only for active sessions and is removed
or harvested on normal exit.
Claude and OpenCode have detailed import notes:
claude-import.md and opencode-import.md.
Codex import scope is summarized in harnesses.md.
Antigravity, Hermes, Qwen, Cursor Agent, and Pi do not import host
~/.gemini/antigravity-cli, host ~/.hermes, host ~/.qwen, host Cursor state,
or host ~/.pi/agent in v1.
Running OpenCode
hazmat harness update opencode
hazmat opencode
hazmat opencode run "summarize this repo"
OpenCode uses the same containment, project preflight, and snapshot flow as the
other harnesses, but it keeps its own provider-specific auth and runtime state.
Use hazmat config import opencode when you want to copy supported host basics
into Hazmat's host-owned secret store.
Running Codex
hazmat harness update codex
hazmat codex
hazmat codex exec "review the recent changes"
Codex uses the same containment and project preflight model. Runtime state lives under the agent user's home directory, while durable imported auth and API keys live in Hazmat's host-owned secret store and are materialized only for active Codex sessions.
Running Antigravity
hazmat harness update antigravity
hazmat antigravity
hazmat antigravity -p "summarize this repo"
Antigravity (agy, Google's successor to the Gemini CLI) uses the same
containment and project preflight model. hazmat harness update antigravity
installs the pinned, checksum-verified agy binary; there is no curated import
in v1. For API-key use, store ANTIGRAVITY_API_KEY (or GEMINI_API_KEY) through
hazmat config agent. Antigravity's Keychain-backed OAuth is reported as an
external, adapter-required boundary.
Running Hermes
hazmat harness update hermes
hazmat hermes
hazmat hermes -- --version
hazmat hermes -- chat "summarize this repo"
Hermes support is experimental and foreground-only. hazmat harness update hermes
verifies a manually installed /Users/agent/.local/bin/hermes; it does not run
an upstream installer or import host ~/.hermes. Hermes sessions use a
project-scoped HERMES_HOME=/Users/agent/.hazmat/hermes/projects/<project-hash>,
receive only allowed provider API-key env vars from Hazmat's credential
registry, and reject gateway/dashboard/API, server, and cron service entrypoints
in v1. Ordinary hazmat rollback preserves that managed Hermes profile tree
with the rest of the agent home; after untrusted Hermes skills, MCP servers,
hooks, or cron-like experiments, the supported full reset is
hazmat rollback --delete-user, then hazmat init and
hazmat harness update hermes.
Running Qwen Code
hazmat harness update qwen
hazmat qwen
hazmat qwen -p "summarize this repo"
Qwen Code is a contained foreground harness. Hazmat installs
@qwen-code/qwen-code@latest into the agent user's local prefix, prepares
/Users/agent/.qwen, and does not import host ~/.qwen auth or settings in
v1. Configure Qwen auth inside the contained profile, or use Qwen-supported
API-key configuration there. Portable QWEN.md and extensions/ assets can
sync from the host on launch.
Running Cursor Agent
hazmat harness update cursor-agent
hazmat cursor-agent
hazmat cursor-agent -- --version
hazmat cursor-agent --print --output-format stream-json --force --trust
Cursor Agent support is foreground/headless and verification-only in v1.
hazmat harness update cursor-agent verifies a manually installed
/Users/agent/.local/bin/cursor-agent; it does not run an upstream installer,
import host Cursor IDE state, copy host ~/.cursor, or grant CURSOR_API_KEY.
Run hazmat cursor-agent -- login or configure Cursor Agent inside the
contained agent profile. Hazmat forwards Cursor Agent flags exactly as provided,
so automation flags such as --force and --trust must be explicit.
Running Pi
hazmat harness update pi
hazmat pi
hazmat pi -- --version
hazmat pi -- --mode rpc
Pi support is foreground and verification-only in v1. hazmat harness update pi verifies a manually installed /Users/agent/.local/bin/pi, prepares
/Users/agent/.pi/agent, and records harness state after pi --version
succeeds. Hazmat does not run an upstream installer, import host ~/.pi/agent,
sync host Pi skills/extensions, or grant Pi provider API-key env vars in v1.
Configure Pi auth, trust, and extensions inside the contained profile. RPC mode
is contained when launched, but Hazmat does not drive Pi's JSON-RPC
prompt/event stream.
Uninstalling
hazmat rollback # remove all system config
hazmat rollback --delete-user --delete-group # also delete agent account
# Remove binaries (choose one):
brew uninstall hazmat # if installed via Homebrew
sudo rm /usr/local/bin/hazmat /usr/local/libexec/hazmat-launch # if installed via script
Your project files are not deleted. Back them up first if needed. Rollback does
remove Hazmat-managed repo-local Git hook state: host approval records,
approved snapshots, per-repo wrappers, and managed .git dispatchers. It
preserves host-owned credential stores and session-time permission repairs unless
a future receipt-aware rollback path explicitly names them for removal.
What the Agent Can and Can't Do
Can:
- Read and write files in your project directory
- Read directories you expose with
-R - Make HTTPS requests to any host
- Run any command available to the agent user
- Access
/private/tmpfor temporary files - Build and run Docker containers (Docker Sandbox mode only)
Can't:
- Read your SSH keys, AWS credentials, GPG keys, or Keychain
- Send email (SMTP blocked), use IRC, FTP, Tor, or VPN protocols
- Access the host Docker daemon (socket locked to your user only)
- Read files outside the approved directories
- Use
sudo
For the current import policy and non-goals, see claude-import.md and opencode-import.md.