# Manual Testing

Hazmat ships an automated test suite (see [docs/testing.md](testing.md)), but the things the test suite can't reach — interactive prompts, real network calls, browser-based OAuth, terminal UI input — need a human at a keyboard.

This page is the human-driven verification checklist. Use it before cutting a release, after touching the harness / seatbelt / init code paths, or when validating a fresh install on a new machine.

## How to use this doc

Each item is a checkbox. Tick it when the **Expected** outcome lands. If something fails, the **On failure** column points at what to inspect next.

You don't have to run the whole list every time. The shortest meaningful pass is **§1 (preconditions) + the harness section for whichever harness you touched**. Run the cross-cutting and recovery sections before a release.

Steps assume macOS, a real TTY (not a piped shell), and a working `hazmat init` from a recent commit. Snippets that need substitution use `<placeholder>` markers.

---

## 1. Preconditions

Before you touch any harness flow.

- [ ] **Latest binary installed**
  - Steps:
    ```bash
    cd ~/workspace/hazmat
    make && make install
    hazmat --version
    ```
  - Expected: version string matches the commit you're testing (e.g. `v0.7.0-NN-g<sha>`).
  - On failure: check `make install` output; ensure `~/.local/bin/hazmat` is on `PATH`.

- [ ] **Init has been run on this machine**
  - Steps:
    ```bash
    hazmat status
    ```
  - Expected: every check is green; agent user exists; sudoers + launch helper present.
  - On failure: run `hazmat init` (interactive) and re-check.

- [ ] **No leaked agent processes from earlier sessions**
  - Steps:
    ```bash
    ps -u agent -o pid,etime,comm | grep -v 'PID\|^$'
    ```
  - Expected: no output, or only short-lived processes you started intentionally. Long-running `hazmat-launch` processes from hours ago indicate a leak (see §6 recovery).
  - On failure: `sudo -n -u agent /usr/bin/pkill -9 -f hazmat-launch`, then re-check.

- [ ] **SSH config is the multi-key shape (not single-key)**
  - Steps:
    ```bash
    grep -A 5 'projects:' ~/.hazmat/config.yaml | head -30
    ```
  - Expected: every project entry under `ssh:` uses the `keys:` list shape (with `name`, `private_key`, `known_hosts`, `hosts`). The single-key shape (`private_key:` directly under `ssh:`) is rejected at config-load time and blocks every harness launch.
  - On failure: migrate any offender to the multi-key shape per [docs/usage.md](usage.md) §"Reusable SSH profiles".

---

## 2. Per-harness flows

Run **one path** (subscription / API key / host import or contained setup) per
harness for a smoke pass; run every supported path before a release.

### 2.1 Claude Code

- [ ] **Bootstrap**
  - Steps: `hazmat bootstrap claude`
  - Expected: each step ✓; `/Users/agent/.local/bin/claude` exists; `claude --version` (run as agent: `sudo -n -u agent -H /Users/agent/.local/bin/claude --version`) prints a version.
  - On failure: re-run with `-v`; check the bootstrap script error.

- [ ] **Subscription path** (`/login`)
  - Preconditions: a Claude Pro/Max subscription on the host.
  - Steps: `hazmat claude` → type `/login` → complete browser OAuth → exit Claude.
  - Expected: `~/.hazmat/secrets/claude/credentials.json` exists after exit; `/Users/agent/.claude/.credentials.json` is gone again once the session ends.
  - Verify: `hazmat claude -p "say only OK"` round-trips.

- [ ] **API key path** (env var)
  - Preconditions: `ANTHROPIC_API_KEY` set in your invoking shell.
  - Steps: `hazmat config agent` → accept the offer to copy the host key → press Enter through git identity.
  - Expected: `~/.hazmat/secrets/providers/anthropic-api-key` exists with mode `0600`; `hazmat claude -p "say only OK"` round-trips.

- [ ] **Host import path**
  - Preconditions: claude already authed on host (`~/.claude/.credentials.json` exists).
  - Steps: `hazmat config import claude --dry-run` → review plan → `hazmat config import claude` (no `--dry-run`).
  - Expected: "Sign-in: yes" in the plan; `~/.hazmat/secrets/claude/credentials.json` and/or `state.json` are populated; subsequent `hazmat claude -p "say OK"` round-trips without a `/login` prompt.

### 2.2 Codex

- [ ] **Bootstrap**
  - Steps: `hazmat bootstrap codex`
  - Expected: each step ✓; `/Users/agent/.codex` and `/Users/agent/.agents` both prepared as `agent:dev 2770`.
  - On failure: check that the GitHub installer URL is reachable; `curl --head` it manually.

- [ ] **Subscription path** (Device Code)
  - Preconditions: a ChatGPT Plus/Pro/Business/Enterprise subscription.
  - Steps: `hazmat codex` → at the auth picker, press `↓` once to highlight **Sign in with Device Code** (or type `2`) → press Enter → complete the device-code flow on host browser.
  - Expected: the highlight moves from option `1` to option `2`; after exit `~/.hazmat/secrets/codex/auth.json` exists and `/Users/agent/.codex/auth.json` has been cleaned up.
  - Verify: `hazmat codex exec "Reply with only OK and nothing else."` returns `OK` with a token-count footer.

- [ ] **API key path** (env var)
  - Preconditions: `OPENAI_API_KEY` set in your invoking shell.
  - Steps: `hazmat config agent` → accept the OpenAI key prompt.
  - Expected: `~/.hazmat/secrets/providers/openai-api-key` exists with mode `0600`; `hazmat codex exec "say OK"` round-trips.

- [ ] **Host import path**
  - Preconditions: codex already authed on host.
  - Steps: `hazmat config import codex` → accept.
  - Expected: `~/.hazmat/secrets/codex/auth.json` exists; subsequent `hazmat codex exec "say OK"` skips the auth picker entirely.

### 2.3 OpenCode

- [ ] **Bootstrap**
  - Steps: `hazmat bootstrap opencode`
  - Expected: each step ✓; PATH shim at `/Users/agent/.local/bin/opencode` → `/Users/agent/.opencode/bin/opencode`; `opencode.json` written.

- [ ] **Subscription / per-provider auth path**
  - Preconditions: a provider account that OpenCode supports (Anthropic, OpenAI, Google, OpenRouter, etc.).
  - Steps: `hazmat opencode` → at the OpenCode prompt, run `/auth` (or exit and use `opencode auth login` outside Hazmat then import).
  - Expected: provider entry is available during the session and harvested into `~/.hazmat/secrets/opencode/auth.json` when the session exits.

- [ ] **API key path** (per provider, no single env var)
  - Note: OpenCode is exempt from the `hazmat config agent` env-var prompts (multi-provider — no single key). Use the host import path or set per-provider keys with `opencode auth login` inside the sandbox.

- [ ] **Host import path**
  - Preconditions: opencode authed on host.
  - Steps: `hazmat config import opencode --dry-run` → `hazmat config import opencode`.
  - Expected: "Sign-in: yes"; `~/.hazmat/secrets/opencode/auth.json` exists; `hazmat opencode run "say OK"` round-trips.

### 2.4 Gemini

- [ ] **Bootstrap**
  - Preconditions: Node.js available on agent PATH (Homebrew node satisfies this).
  - Steps: `hazmat bootstrap gemini`
  - Expected: each step ✓; `/Users/agent/.local/bin/gemini` linked from npm prefix; `/Users/agent/.gemini` prepared.
  - On failure: check `node --version` works for the agent: `sudo -n -u agent -H bash -lc 'node --version'`.

- [ ] **Subscription path** (Google sign-in)
  - Preconditions: a Google account with Gemini access.
  - Steps: `hazmat gemini` → "Sign in with Google" flow.
  - Expected: if Gemini writes file-backed auth, Hazmat harvests that data into `~/.hazmat/secrets/gemini/` when the session exits. If Gemini stores OAuth only in Keychain, Hazmat reports that as an adapter-required external backend and does not copy the Keychain item into `/Users/agent`.

- [ ] **API key path** (env var)
  - Preconditions: `GEMINI_API_KEY` set in your invoking shell (get one from https://aistudio.google.com/apikey).
  - Steps: `hazmat config agent` → accept the Gemini key prompt.
  - Expected: `~/.hazmat/secrets/providers/gemini-api-key` exists with mode `0600`; `hazmat gemini -p "say OK"` round-trips.

- [ ] **Host import path** (file-fallback only)
  - Preconditions: host stores creds in `~/.gemini/oauth_creds.json` (file fallback). If host uses macOS Keychain, this item is N/A — the import skips the OAuth file because Keychain-backed Gemini OAuth is not imported yet.
  - Steps: `hazmat config import gemini`.
  - Expected: imported file-backed auth lands in `~/.hazmat/secrets/gemini/`; `hazmat gemini -p "say OK"` round-trips.

### 2.5 Hermes (experimental)

- [ ] **Bootstrap detects a missing binary without mutating state**
  - Steps: on a machine without `/Users/agent/.local/bin/hermes`, run `hazmat bootstrap hermes`.
  - Expected: the command exits with manual install guidance, does not run an upstream installer, and does not record Hermes as installed.

- [ ] **Bootstrap verifies a manually installed binary**
  - Preconditions: an agent-owned Hermes executable is present at `/Users/agent/.local/bin/hermes`.
  - Steps: `sudo -n -u agent -H /Users/agent/.local/bin/hermes --version` → `hazmat bootstrap hermes`.
  - Expected: `hermes --version` succeeds as the agent user; bootstrap records Hermes only after that version probe succeeds; re-running bootstrap is idempotent.

- [ ] **Provider key reuse**
  - Preconditions: one or more of `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `GEMINI_API_KEY`, or `OPENROUTER_API_KEY` is set in the invoking shell.
  - Steps: `hazmat config agent` → accept the provider-key prompt(s); then run `hazmat explain --for hermes --json -C /tmp`.
  - Expected: explain JSON contains redacted provider grants for Hermes and a session note for project-scoped `HERMES_HOME=/Users/agent/.hazmat/hermes/projects/<project-hash>`. Raw key values never appear.

- [ ] **Managed state root and no host profile import**
  - Preconditions: optional sentinel file under host `~/.hermes/` to prove host profile state is not copied.
  - Steps: `hazmat explain --for hermes -C /tmp`; run `hazmat hermes --no-backup -- --version`; then inspect `/Users/agent/.hazmat/hermes/projects`, `/Users/agent/.hermes`, and `hazmat config import --help`.
  - Expected: a project-scoped directory exists under `/Users/agent/.hazmat/hermes/projects`; `/Users/agent/.hermes` is not created from host `~/.hermes`; host sentinel files are unchanged; `hazmat config import` has no `hermes` target.

- [ ] **Rejected service entrypoints**
  - Steps: run `hazmat hermes -- gateway`, `hazmat hermes -- dashboard`, `hazmat hermes -- server`, and `hazmat hermes -- cron add daily`.
  - Expected: each command fails before launch with foreground-only guidance. Ordinary foreground commands such as `hazmat hermes -- --version` or `hazmat hermes -- chat ...` still pass through.

- [ ] **Native `--network none`**
  - Steps: `hazmat hermes --network none --metadata-json --no-backup -- --version`.
  - Expected: stderr includes one JSON metadata line with `"requested":"none"`, `"effective":"none"`, and `"enforced":true`; the Hermes foreground command still exits cleanly. A provider-backed Hermes request should fail closed if it tries to dial out.

### 2.6 Qwen Code

- [ ] **Bootstrap**
  - Preconditions: Node.js 20 or newer available on the agent PATH.
  - Steps: `hazmat bootstrap qwen`
  - Expected: each step ✓; `/Users/agent/.local/bin/qwen` exists; `/Users/agent/.qwen` is prepared; bootstrap does not import host `~/.qwen`.
  - On failure: check `node --version` works for the agent: `sudo -n -u agent -H bash -lc 'node --version'`.

- [ ] **Contained auth path**
  - Steps: `hazmat qwen` → complete Qwen's auth/config flow inside the contained session → exit.
  - Expected: Qwen state is under `/Users/agent/.qwen`; host `~/.qwen` auth/settings are unchanged and not copied into Hazmat's secret store.
  - Verify: `hazmat qwen -p "say only OK"` round-trips.

- [ ] **No host import path**
  - Preconditions: optional sentinel file under host `~/.qwen/` to prove host profile state is not imported.
  - Steps: `hazmat explain --for qwen -C /tmp`; run `hazmat qwen --no-backup -p "say OK"`; inspect `/Users/agent/.qwen` and `hazmat config import --help`.
  - Expected: the contained profile exists; host sentinel files are unchanged; `hazmat config import` has no `qwen` target. Only portable Qwen assets such as `QWEN.md` and `extensions/` may sync when present.

- [ ] **Approval-mode forwarding**
  - Steps: `hazmat qwen --no-backup -p "say OK"` and `hazmat qwen --no-backup --yolo -p "say OK"`.
  - Expected: when `session.skip_permissions` is enabled, Hazmat prepends `--yolo` only when it is not already present. The second command must not pass duplicate `--yolo` flags.

- [ ] **Native `--network none`**
  - Steps: `hazmat qwen --network none --metadata-json --no-backup -p "say OK"`.
  - Expected: stderr includes one JSON metadata line with `"requested":"none"`, `"effective":"none"`, and `"enforced":true`; a provider-backed Qwen request should fail closed if it tries to dial out.

### 2.7 Cursor Agent

- [ ] **Bootstrap**
  - Preconditions: Cursor Agent CLI is installed or linked for the agent user at `/Users/agent/.local/bin/cursor-agent`.
  - Steps: `hazmat bootstrap cursor-agent`
  - Expected: each step ✓; bootstrap records Cursor Agent after `cursor-agent --version`; bootstrap does not import host Cursor IDE state, host `~/.cursor`, or host auth settings.

- [ ] **Contained auth path**
  - Steps: `hazmat cursor-agent -- login` → complete Cursor Agent's auth/config flow inside the contained session → exit.
  - Expected: Cursor Agent state is under `/Users/agent` paths; host Cursor IDE auth/profile state is unchanged and not copied into Hazmat's secret store.
  - Verify: `hazmat cursor-agent -- --version` round-trips, and a headless command works when you pass Cursor Agent's own `--print`/`--force`/`--trust` flags.

- [ ] **No host import path**
  - Preconditions: optional sentinel file under host `~/.cursor/` to prove host profile state is not imported.
  - Steps: `hazmat explain --for cursor-agent -C /tmp`; run `hazmat cursor-agent --no-backup -- --version`; inspect `/Users/agent` Cursor state paths and `hazmat config import --help`.
  - Expected: host sentinel files are unchanged; `hazmat config import` has no `cursor-agent` target; no Cursor host assets are synced.

- [ ] **Native `--network none`**
  - Steps: `hazmat cursor-agent --network none --metadata-json --no-backup -- --version`.
  - Expected: stderr includes one JSON metadata line with `"requested":"none"`, `"effective":"none"`, and `"enforced":true`; a provider-backed Cursor Agent request should fail closed if it tries to dial out.

---

## 3. Cross-cutting

These exercise the per-harness scaffolding rather than any one harness.

- [ ] **`hazmat init --bootstrap-agent <harness>` end-to-end**
  - Steps: on a clean (rolled-back) machine: `hazmat rollback --yes` → `hazmat init --bootstrap-agent gemini` (try each of `claude / codex / opencode / gemini / hermes / qwen / cursor-agent` in turn).
  - Expected: agent user created; bootstrap step runs for the chosen harness; `hazmat config agent` prompt appears; the optional "Import basics?" prompt appears only for importable harnesses; the "Ready to use" guidance ends with `cd your-project && hazmat <harness>`. Hermes and Cursor Agent init expect manual binary paths and do not offer profile import. Qwen installs through npm, prepares `/Users/agent/.qwen`, and does not offer profile import.

- [ ] **`hazmat explain --for <harness>`** for each of `claude / codex / opencode / gemini / hermes / qwen / cursor-agent / shell / exec`
  - Steps: `hazmat explain --for <each> -C /tmp` (or any project dir without an SSH-config gate)
  - Expected: each prints a session contract; integrations section updates if `--integration go` is added; no errors.

- [ ] **Harness lifecycle CLI**
  - Steps: run `hazmat harness status`; for each built-in harness, run `hazmat harness status <harness>` and `hazmat harness update <harness> --dry-run`; on a disposable agent setup, run `hazmat harness uninstall <harness> --dry-run`.
  - Expected: list status shows all six harnesses; detail status shows binary/probe, recorded state, import status, credential hint, managed-code paths, and preserved data. Update dry-run follows the same path as bootstrap without mutating state. Uninstall dry-run lists only declared Hazmat-owned code artifacts plus metadata removal and says auth/profile/session data is preserved. Hermes does not claim Hazmat owns or removes the manual Hermes binary; Qwen preserves `/Users/agent/.qwen` profile state while only declaring Hazmat-owned npm code artifacts.

- [ ] **Docker Sandbox support across harnesses**
  - Preconditions: repo with a `Dockerfile`.
  - Steps: `hazmat codex --docker=auto -C <repo>` (repeat for `opencode`, `gemini`, `hermes`, `qwen`, and `cursor-agent`), then explicitly `hazmat codex --docker=sandbox -C <repo>` (repeat for `opencode` / `gemini` / `hermes` / `qwen` / `cursor-agent`).
  - Expected: `--docker=auto` routes the matching harness into Docker Sandbox mode on Docker-heavy private-daemon repos; explicit `--docker=sandbox` launches the same harness in Docker Sandbox mode without redirecting you to Claude.

- [ ] **Per-harness seatbelt scoping**
  - Steps: snapshot the generated SBPL for claude vs codex:
    ```bash
    hazmat explain --for claude --json -C /tmp/x | jq -r '.session.script_path' || true
    # Or eyeball: launch a session, then inspect /tmp/hazmat-<pid>.sb while it's live.
    ```
  - Expected: the codex policy contains `com.apple.SystemConfiguration.configd`, `com.apple.SecurityServer`, `/Library/Keychains`, and the `apple.shm.notification_center` IPC; the claude policy does **not** contain any of those (least-privilege gating from `sandboxing-m7f7`).

- [ ] **Session integrations apply uniformly per harness**
  - Steps: in a Go project, `hazmat explain --for codex`, `hazmat explain --for gemini`, `hazmat explain --for hermes`, `hazmat explain --for qwen`, and `hazmat explain --for cursor-agent`.
  - Expected: all five show `Integrations: go` with the same `Integration sources` line; all auto-add the Go module cache to read-only.

- [ ] **Harness asset sync**
  - Preconditions: edit a file in your host `~/.codex/prompts/` (or `~/.claude/commands/` for claude, `~/.gemini/extensions/` for gemini, `~/.config/opencode/commands/` for opencode, `~/.qwen/QWEN.md` or `~/.qwen/extensions/` for qwen).
  - Steps: launch the matching `hazmat <harness>` session; observe the "host changes" line.
  - Expected: a "<Harness> asset sync" entry; the agent-side file matches the host-side after launch. For Hermes and Cursor Agent, expected result is the inverse: no asset-sync entry and no host profile copy.

- [ ] **Pre-session snapshot**
  - Steps: any `hazmat <harness>` launch; before chatting, scroll up to the snapshot line.
  - Expected: `Snapshot: <project> ... done (X.Xs)`; `hazmat snapshots list` shows the new entry.

- [ ] **Cloud credential storage**
  - Preconditions: disposable S3-compatible test credentials, or run only the config/migration checks without `hazmat backup --cloud`.
  - Steps: configure with `HAZMAT_CLOUD_SECRET_KEY=<secret> HAZMAT_CLOUD_PASSWORD=<recovery> hazmat config cloud --endpoint <endpoint> --bucket <bucket> --access-key <access> --secret-key-from-env`.
  - Expected: output says no S3 objects are written until the first backup; `~/.hazmat/secrets/cloud/s3-access-key-id`, `s3-secret-key`, and `kopia-recovery-key` exist with mode `0600`; `~/.hazmat/config.yaml` contains endpoint/bucket but not the access key, secret key, recovery key, `access_key:`, `recovery_key:`, or `password:`. The bucket may still show `0 byte` until `hazmat backup --cloud -C <project>` succeeds for a selected project. If an old `~/.hazmat/cloud-credentials` file is present, the next cloud read migrates it into `~/.hazmat/secrets/cloud/s3-secret-key` and removes the legacy file.

- [ ] **Project-scoped cloud backup and restore**
  - Preconditions: disposable S3-compatible test credentials and a small disposable project directory.
  - Steps: run `hazmat backup --cloud -C <project>`; modify a file in `<project>`; run `hazmat restore --cloud -C <project>` with confirmation.
  - Expected: backup output names `<project>`; restore snapshots current `<project>` state before overwrite; restore affects only `<project>`, not sibling directories under the same parent.

- [ ] **Credential inventory and legacy residue**
  - Preconditions: at least one configured provider key or imported harness auth; optional old residue under `/Users/agent/.zshrc`, `/Users/agent/.config/git/credentials`, `~/.hazmat/cloud-credentials`, or `~/.hazmat/ssh/keys` for migration checks.
  - Steps: run `hazmat migrate credentials --dry-run`; run `hazmat migrate credentials`; run `hazmat check`; inspect the "Credential inventory" section.
  - Expected: every credential surface is reported by registry ID with backend, delivery mode, and host-store presence/absence. Legacy locations are migrated or produce actionable repair guidance. Output must not contain raw API keys, OAuth tokens, PATs, S3 keys, recovery keys, or SSH private-key material.

- [ ] **Git HTTPS credential broker**
  - Preconditions: a disposable HTTPS Git token, or an existing legacy `/Users/agent/.config/git/credentials` fixture.
  - Steps: launch any native session and run a Git HTTPS operation that needs credentials; exit; run `hazmat check`.
  - Expected: `~/.hazmat/secrets/git-https/credentials` exists with mode `0600`; `/Users/agent/.config/git/credentials` is absent after migration; `hazmat check` reports `git-https.agent-store` without printing the token. Agent `.gitconfig` has no persistent `credential.helper = store --file /Users/agent/.config/git/credentials`; sessions inject the brokered helper at launch.

- [ ] **Git SSH transport broker**
  - Preconditions: a project configured with `hazmat config ssh add --host <git-host> <key>`; optional `~/.ssh/config` alias using `ProxyJump`.
  - Steps: launch a native session for that project; run a Git SSH fetch or push against the configured host; inside the session run `test -z "${SSH_AUTH_SOCK:-}" && find /Users/agent -type s -name '*agent*' -print`.
  - Expected: Git SSH succeeds through `GIT_SSH_COMMAND`; `SSH_AUTH_SOCK` is unset; no readable session ssh-agent socket exists under `/Users/agent`; aliases and supported `ProxyJump` routing continue to work.

- [ ] **Explicit GitHub API capability**
  - Preconditions: disposable fine-grained GitHub token with minimal read scope.
  - Steps: `GH_TOKEN=<token> hazmat config github --token-from-env`; run `hazmat explain --github --json`; run `hazmat shell --github --no-backup` and `test -n "$GH_TOKEN" && test -z "${GITHUB_TOKEN:-}" && echo OK` inside the session; then try `hazmat explain --github --docker=sandbox`.
  - Expected: `~/.hazmat/secrets/github/token` exists with mode `0600`; explain JSON includes one redacted `github.api-token` grant for `GH_TOKEN`; the native session sees `GH_TOKEN` but not `GITHUB_TOKEN`; Docker Sandbox preview fails closed with an explicit unsupported-capability message. Output must not print the raw token.

- [ ] **Native `--network none` egress denial**
  - Steps: run `hazmat explain --network none --json` and confirm `network_policy.requested` and `network_policy.effective` are both `none`; then run `hazmat exec --network none --metadata-json --no-backup -- /bin/zsh -lc 'curl -fsS --max-time 5 https://example.com >/tmp/hazmat-net.out'` and capture stderr.
  - Expected: the command fails closed; stderr includes one JSON metadata line with `"requested":"none"`, `"effective":"none"`, `"enforced":true`, and denied surfaces for outbound IPv4, outbound IPv6, and DNS. Harness stdout remains separate from Hazmat's metadata.

- [ ] **Native `--network none` DNS and teardown**
  - Steps: run `hazmat exec --network none --metadata-json --no-backup -- /bin/zsh -lc 'nc -z -G 5 example.com 443'` and `hazmat exec --network none --metadata-json --no-backup -- /bin/zsh -lc 'nc -z -G 5 93.184.216.34 443'`; interrupt a third long-running `hazmat exec --network none --metadata-json --no-backup -- /bin/zsh -lc 'sleep 60'` with Ctrl-C; then run a default `hazmat exec --no-backup -- /bin/zsh -lc 'echo OK'`.
  - Expected: DNS-name and direct IPv4 dials both fail in network-none sessions; interruption leaves no per-session firewall/pf state to clean up; the subsequent default session launches normally.

- [ ] **Status bar visible during an interactive session**
  - Steps: `hazmat claude` (or any harness) in a fullscreen terminal; check the bottom row.
  - Expected: `☢ HAZMAT │ <integrations> ... <project>` rendered in the bottom row, doesn't scroll.

---

## 4. Self-heal scenarios (regression coverage)

These verify that earlier-fixed bugs stay fixed.

- [ ] **Legacy auth file migrates into host-owned storage**
  - Setup: simulate an older Hazmat release that still left auth under `/Users/agent`:
    ```bash
    rm -f ~/.hazmat/secrets/codex/auth.json
    sudo -u agent rm -f /Users/agent/.codex/auth.json
    sudo -u agent install -d -m 700 /Users/agent/.codex
    sudo -u agent cp ~/.codex/auth.json /Users/agent/.codex/auth.json
    ```
  - Steps: `hazmat codex exec "Reply with only OK."`
  - Expected: the run succeeds; after exit `~/.hazmat/secrets/codex/auth.json` exists and `/Users/agent/.codex/auth.json` is gone again.
  - On failure: check the session notes for a migration warning or mismatch warning from the harness auth runtime.

- [ ] **Codex bootstrap creates `~/.agents` shared dir** (regression: `sandboxing-3u4a`)
  - Steps: `sudo -u agent rm -rf /Users/agent/.agents` → `hazmat bootstrap codex`
  - Expected: re-creates `/Users/agent/.agents` as `agent:dev 2770`; subsequent `hazmat codex` does not fail with `mkdir /Users/agent/.agents: permission denied`.

- [ ] **`hazmat-launch` does not hang in non-TTY shells** (regression: `sandboxing-qfv6`)
  - Steps: from a non-TTY shell (e.g. an SSH session piped to `tee`, or a script):
    ```bash
    hazmat codex exec "Reply with only OK." 2>&1 | tee /tmp/codex-noninteractive.log
    ```
  - Expected: completes within tens of seconds; `tee` shows the OK response. **Never hangs** spinning at 70%+ CPU.
  - On failure: check `closeInheritedFDs` is using `/dev/fd` enumeration (not iterating to RLIMIT_NOFILE); `ps -u agent` should not show stuck `hazmat-launch exec ...` processes after the run.

- [ ] **Config-agent with multiple harnesses installed**
  - Preconditions: claude + codex + gemini + hermes + qwen + cursor-agent all bootstrapped.
  - Steps: `hazmat config agent`
  - Expected: provider-key prompts are de-duplicated by env var. Claude, Codex, Gemini, and Hermes can share the same stored provider key when the harness is an allowed consumer. OpenCode, Qwen, and Cursor Agent are intentionally skipped in v1 (no single Hazmat-managed provider env var).

---

## 5. Rollback / cleanup

- [ ] **`hazmat rollback`** unwinds init cleanly
  - Steps: `hazmat rollback --yes` → `hazmat status`
  - Expected: agent user removed; sudoers + DNS blocklist + pf anchor + launch helper all removed; status shows "not initialized".

- [ ] **Idempotent re-bootstrap after rollback**
  - Steps: after a rollback, `hazmat init --bootstrap-agent claude` (or any harness)
  - Expected: works without manual cleanup of leftover files.

---

## 6. Recovery (when things are stuck)

Reference card for fixing common stuck states. Not a checklist — these are the moves to make when something in §1–§5 fails.

- **Stuck `hazmat-launch` agent processes** (high CPU, won't exit):
  ```bash
  sudo -n -u agent /usr/bin/pkill -9 -f hazmat-launch
  ```

- **Single-key SSH config blocking all launches**: edit `~/.hazmat/config.yaml`, convert each project entry to the multi-key shape ([docs/usage.md](usage.md) §"Reusable SSH profiles"). The error message includes the exact YAML to paste.


- **Codex chat fails with `no native root CA certificates`**: rebuild against latest master (commit `eaaaa1c` and later). Several Security framework / mach-service allowances were added.

- **Stale auth file left under `/Users/agent` from an older build**: relaunch the matching harness with current Hazmat. Startup recovery should promote the leftover agent copy into `~/.hazmat/secrets`, archive any divergent host-owned copy under a sibling `.conflicts/` directory, and remove the agent copy. Manual removal should only be needed if recovery reports a filesystem error:
  ```bash
  hazmat <harness>
  ```

---

## 7. After-test cleanup

- [ ] **No leaked agent processes**: `ps -u agent` shows nothing surprising.
- [ ] **No stray `/tmp/hazmat-*.sb` files older than your session**: `ls -lt /tmp/hazmat-*.sb 2>/dev/null | head` — anything older than your last session was leaked by an earlier crash.
- [ ] **Snapshot count is what you expected**: `hazmat snapshots list | wc -l` — sanity check that pre-session snapshots aren't accumulating without bound.
- [ ] **bd issues reflect the test result**: if a checklist item failed in a way that points at a bug, file it (`bd create --title="..." --type=bug ...`) before moving on.

---

## What this doc deliberately doesn't cover

- **Linux**: there's no `hazmat init` on Linux yet; `sandboxing-pk5x` tracks adding it.
- **Docker Sandbox mode**: see [docs/tier3-docker-sandboxes.md](tier3-docker-sandboxes.md) — separate test discipline.
- **CVE coverage**: see [docs/cve-audit.md](cve-audit.md).
- **TLA+ verification**: `cd tla && bash check_suite.sh` — no human-in-loop component.
- **Performance**: there's no perf test yet; if a session takes noticeably longer to start than the last release, file a bug.

If something feels wrong but isn't covered above, file an issue with `bd create --title="..." --type=bug` rather than expanding this doc indefinitely. This doc should stay short enough to skim before a release.
