# Security

Safety model for autonomous hermit agents — deny patterns, defense in depth, and operational recommendations.

---

> **Important:** Deny patterns are blocklist-based string matches. They block known dangerous commands but cannot prevent all harmful invocations. The default `auto` mode reduces (but does not eliminate) the need for container isolation by requiring classifier review of each action. Always use container isolation when running with `bypassPermissions`. See [Known Limitations](#known-limitations) below.

## Deny Patterns

Block tool invocations regardless of permission mode. Generated by `/claude-code-hermit:hatch` (step 9) or `/claude-code-hermit:docker-setup`. Canonical source: `state-templates/deny-patterns.json`.

### Default set (all deployments)

```json
{
  "permissions": {
    "deny": [
      "Bash(rm -rf *)",
      "Bash(chmod 777*)",
      "Bash(*sudo *)",
      "Bash(*> /etc/*)",
      "Bash(curl * | bash*)",
      "Bash(wget * | bash*)",
      "Bash(env)",
      "Bash(printenv)",
      "Bash(cat .env*)",
      "Bash(cat */.env*)",
      "Bash(cat ~/.ssh/*)",
      "Bash(cat ~/.aws/*)",
      "Bash(*API_KEY*)",
      "Bash(*SECRET*)",
      "Bash(*TOKEN*)",
      "Edit(**/.claude-code-hermit/OPERATOR.md)",
      "Write(**/.claude-code-hermit/OPERATOR.md)"
    ]
  }
}
```

### Always-on additions (Docker / tmux)

```json
{
  "permissions": {
    "deny": [
      "Bash(ssh *)",
      "Bash(docker *)",
      "Bash(kubectl *)",
      "Bash(npm publish*)",
      "Bash(git push --force*)",
      "Bash(git push origin main*)",
      "Bash(git reset --hard*)",
      "Bash(*--no-verify*)"
    ]
  }
}
```

`/claude-code-hermit:docker-setup` applies both sets (default + always-on). `/claude-code-hermit:hatch` hardened mode applies both sets minus `docker/kubectl/ssh` (valid on host). Hatch minimal mode applies the default set only.

---

## Defense in Depth

| Layer               | Where           | Enforcement           |
| ------------------- | --------------- | --------------------- |
| Deny patterns       | `settings.json` | Mechanical*           |
| Agent-level rules   | `agents/*.md`   | Instruction-following |
| Hook enforcement    | `hooks.json`    | Mechanical            |
| Config isolation    | Named volume    | Mechanical            |
| Container isolation | Docker/VM       | Mechanical            |
| Container hardening | docker-compose.hermit.yml | Kernel-enforced (defense in depth — `no-new-privileges` is load-bearing; cap drops + PID cap are incremental) |
| Advanced hardening (opt-in) | docker-compose.security.yml | Kernel-enforced (LAN block + DNS policy, resource bounds, sysctls — opt-in via `/docker-security`; LAN containment is the load-bearing layer here) |
| OPERATOR.md         | OPERATOR.md     | Instruction-following |

*\* **Deny rule caveat:** There are reported bugs where `Edit`/`Write` deny rules in `settings.json` are not always enforced — Claude Code has been observed editing files that match a deny pattern. Deny rules are the correct approach and best available option, but can't be treated as airtight today. For OPERATOR.md specifically, Claude Code's built-in `.claude/` directory protection adds a second layer. Combined, OPERATOR.md modification is unlikely but not impossible until Anthropic fixes deny rule enforcement.*

---

## Container Hardening

`/claude-code-hermit:docker-setup` (v1.0.26+) renders three kernel-enforced hardening stanzas into `docker-compose.hermit.yml`. These are defense in depth — they reduce the blast radius of a successful prompt injection or a compromised plugin. They are **not** a substitute for the upstream defenses (deny patterns, non-root user, named-volume isolation, no host secret mounts) and they don't make the container "safe against prompt injection." They make a successful injection less useful.

Honest weighting:

| Stanza                                   | What it does                                                                   | Real teeth?                                                                                                 |
| ---------------------------------------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| `security_opt: [no-new-privileges:true]` | Blocks setuid escalation at the kernel level, even via planted setuid binaries | **Yes** — closes a real vector against future supply-chain compromise of any installed plugin              |
| `cap_drop: ALL`                          | Revokes Linux capabilities (NET_RAW, SETUID, DAC_OVERRIDE, …)                  | Incremental — non-root `claude` user already had no ambient caps; this closes the kernel-enforced ceiling  |
| `pids_limit: 2048`                       | Caps process count (fork-bomb / runaway-subprocess bound)                      | Resource bound, not a security primitive on its own — limits collateral damage                             |

Hermit's runtime (tmux, claude CLI, python3, jq, npm, git+HTTPS plugin installs) needs none of what these stanzas remove. Steady-state PID usage is ~80; install bursts can spike higher but stay well below 2048.

**Override path.** Operators with custom workloads (services on ports <1024, setuid helpers, high-PID frameworks) edit `docker-compose.hermit.yml` directly to relax or remove the relevant stanza. The defaults are conservative because the recommended-plugins flow accepts third-party marketplaces — every operator who didn't audit every plugin benefits from the floor.

**Verifying it's active.** From the host:

```bash
# Capabilities — both should be 0000000000000000
docker compose -f docker-compose.hermit.yml exec hermit \
  grep -E '^Cap(Eff|Bnd)' /proc/self/status

# no-new-privileges — should report NoNewPrivs: 1
docker compose -f docker-compose.hermit.yml exec hermit \
  grep NoNewPrivs /proc/self/status

# PID limit — should report 2048
docker compose -f docker-compose.hermit.yml exec hermit cat /sys/fs/cgroup/pids.max
```

**Limitation.** These flags only apply at container creation, not on `restart`. After patching an existing compose file, run `hermit-docker down` then `hermit-docker up` — `hermit-docker restart` silently keeps the old container.

### Advanced hardening — `/docker-security`

Beyond the always-on baseline above, `/claude-code-hermit:docker-security` is an opt-in advanced wizard that adds a `docker-compose.security.yml` overlay with LAN containment + DNS policy (firewall + DNS sidecar with port-53 redirect for actual enforcement), resource bounds + kernel hygiene sysctls, and a boot-time plugin-install audit log. Each toggle is presented with honest cost/benefit framing, applied as an overlay (does not modify `docker-compose.hermit.yml`), verified against the live container, and fully reversible. The LAN containment toggle is hard-skipped when `docker.network_mode: "host"` is in use. Honest limitation surfaced in the wizard and dedicated docs: DNS policy blocks domain-based exfil but cannot stop direct-IP egress to a hardcoded public address. See [Docker Security](docker-security.md) for the full toggle reference, verify commands, and documented limitations.

---

## Recommendations

- **Strict hook profile** for always-on agents (no performance penalty)
- **Session budget** of $5-10 for overnight sessions
- **Heartbeat** for 24/7 monitoring — detects stalled agents, changed conditions, rate limits
- **Network:** allow only `api.anthropic.com`, your channel API, and `github.com` when possible
- **Secrets:** don't mount `~/.aws/`, `~/.ssh/` into containers. Use scoped API keys.
- **Review session reports** before pushing — check for leaked paths, credentials, or connection strings

---

### Known limitations

- **Egress filtering is opt-in.** Bridge networking isolates from localhost but the container can still reach the internet by default. A successful prompt injection can exfiltrate data to external hosts. **Mitigation available**: `/claude-code-hermit:docker-security` adds LAN containment + DNS policy via a firewall sidecar with kernel-level port-53 redirect for real enforcement. **Remaining gap**: direct public-IP egress (hardcoded IPs, no DNS) is not blocked even with the wizard — v1.1 may add nftset-driven IP allowlisting. See [Docker Security](docker-security.md).
- **No input sanitization.** Content fetched from external APIs enters Claude's context unsanitized. Deny patterns cannot catch injection via fetched content — that's an instruction-following boundary, not a mechanical one. Future: pre-processing hook that strips known injection patterns.
- **Deny patterns are a blocklist.** They catch commands you anticipated. Equivalent commands not on the list execute normally. Bind mount scope and network isolation are stronger defenses for novel attacks.

---

## Security Checklist

- [ ] Docker/VM if using `permission_mode: "bypassPermissions"` — see [Always-On Setup](always-on.md)
- [ ] Non-root user inside container
- [ ] Container hardening stanzas present (`no-new-privileges`, `cap_drop`, `pids_limit`) — generated by `/docker-setup` v1.0.26+
- [ ] Advanced hardening considered (`/docker-security`) — at minimum LAN containment if hermit runs on a home/office network alongside HA / NAS / personal devices
- [ ] Deny patterns configured
- [ ] Config isolation via named volume (default) — no host `~/.claude` bind-mount
- [ ] No host mounts beyond project directory
- [ ] No production credentials accessible
- [ ] Strict hook profile
- [ ] Session budget set
- [ ] Heartbeat enabled
- [ ] OPERATOR.md includes approval constraints and hard rules
- [ ] Session reports reviewed before pushing
- [ ] `state/` directory gitignored (contains runtime observations, not config)
- [ ] Only trusted marketplaces in `docker.recommended_plugins` (review entries periodically via `/hermit-settings docker`)

## Plugin Security

### Trust Model

Installing a plugin runs that plugin's hooks and skills with the same authority as hermit. Only install plugins from sources you trust the same as hermit itself. The container hardening in `docker-compose.hermit.yml` (`cap_drop`, `no-new-privileges`, `pids_limit`) reduces what a *compromised* plugin can do to the host kernel — it does not vet the plugin's intent, sandbox its file access within the project, or prevent it from acting on your behalf via the agent. A malicious or careless plugin still runs as you.

Plugins run with the same permissions as Hermit. The default `auto` mode applies classifier review to each action; if the operator has opted into `bypassPermissions` for fully unattended Docker operation, plugins run with full unrestricted execution — no permission prompts, no human review.

**Policy:** The entrypoint installs every enabled entry in `docker.recommended_plugins` on first boot, regardless of marketplace. The trust gate is at **configuration time**: entries only land in `config.json` when the operator explicitly confirmed the mirrored plugin list during `/docker-setup` or `/hermit-settings docker`. Preselection during setup is restricted to `claude-plugins-official` and `gtapps/*` — third-party entries must be manually opted in. See [Recommended Plugins — Trust model](recommended-plugins.md#trust-model).

**No auto-updates:** The entrypoint installs plugins on first boot but never auto-updates them. Plugin updates are operator-initiated via `/claude-code-hermit:hermit-evolve` or image rebuild.

**Hermit does not vet, audit, or take responsibility for any plugin.** Even official plugins should be understood before enabling. You are responsible for evaluating what you install.

See [Recommended Plugins](recommended-plugins.md) for the full list and installation instructions.
