---
title: "Threat Model"
description: "Why AI agents need containment, what attack surfaces clawker mitigates, and what you're responsible for"
icon: "crosshairs"
---

AI coding agents are not traditional CLI tools. They make autonomous decisions about what files to read and write, what commands to run, what network requests to make, and what credentials to use. They do this with full conviction — even when they're wrong. They hallucinate file paths, lose context mid-task, get tunnel vision on a broken approach, and can be coerced by adversarial content embedded in the data they fetch. Handing one of these agents unrestricted code execution, full filesystem access, and open network on your bare-metal host is reckless.

Clawker exists because containers have been solving this problem for over a decade. The agent runs inside a Docker container where it can thrash, experiment, and make mistakes freely — that's the whole point of a devcontainer. What matters is that the blast radius stops at the container boundary. The host filesystem, credentials, and network are protected by default, and every capability is restricted until you explicitly open it.

This page maps the attack surfaces to clawker's security controls. For configuration details and CLI commands, follow the cross-references to the dedicated guides.

## Threat Actors

Clawker's security model is designed around two threat actors:

**The agent itself.** An LLM-powered coding agent is autonomous software that can hallucinate file paths, run destructive commands, or pursue the wrong goal with absolute confidence. It doesn't need to be malicious to cause damage — context window amnesia, tunnel vision, and overconfident tool use are enough. The agent might `rm -rf` a directory it shouldn't touch, push code to the wrong branch, or install packages that compromise the environment, all while believing it's doing exactly what you asked.

**Cybercriminal Prompters.** This might seem facetious, or an oversimplification, but when you distill it down basically any external entity is trying to coerce your agent via prompt injection. There are a myriad of subcategories and motivations here but the same controls deal with all of them. 

## Attack Vectors 

### Prompt injection

Adversarial content embedded in web pages, README files, package metadata, API responses, or files hosted on cloud storage can hijack the agent's intent. A malicious instruction hidden in a fetched document could direct the agent to exfiltrate source code, steal credentials, or modify files in ways that serve the attacker rather than the user. The agent cannot reliably distinguish legitimate instructions from injected ones.

**Clawker does not attempt to prevent prompt injection** — and this is a deliberate design choice. LLMs can develop their own specialized internal languages — often referred to as "cryptoglyphs" or "internal representations" — which are highly efficient, non-human, mathematical, or symbolic languages. They can also interpret ancient Hebrew, binary, base64, steganographic patterns, and every other human or machine dialect in history. Trying to filter or detect injected instructions is boiling the ocean (actually, boiling the ocean might be easier).

Similarly, **clawker does not attempt to inspect egress traffic for sensitive content.** Building what amounts to a reverse WAF that can detect whether an outgoing request — across all protocols, all ports, of any size — contains something sensitive is equally fruitless. HTTP has no technical size limit on request bodies, and other protocols vary or have none at all. Scanning every byte of outbound traffic hoping to detect a highly obfuscated and encrypted payload that *might* contain sensitive data would make the system unusable. Even large WAF vendors cap their request scanning buffers to small allocations (typically 128KB max) because full-payload inspection at scale is impractical. The payload could be a tar archive of base64-triple-encoded text containing secrets, double-encrypted, split across multiple requests, or disguised as legitimate API traffic. Content-based exfiltration detection is an arms race you cannot win. Enlisting another LLM to review outbound requests doesn't solve this either — it adds enormous latency to every network call and relies on a model that is vulnerable to the exact same coercion and precision problems as the agent it's supposed to be watching.

Instead, clawker takes the only approach that actually works: **deny the connection entirely.** If the agent gets coerced into exfiltrating data, the firewall blocks the egress — it doesn't matter what's in the payload because the payload never leaves. If it gets coerced into destructive file operations, container isolation limits the blast radius to the mounted workspace. The injection can change the agent's intent, but it can't change the container's constraints.

## Supply Chain Security

The clawker project leverages every version pinning and integrity check opportunity in CI, its infra containers, and embedded Dockerfile template. SCA and SAST scanning checks are used in CI.

The release pipeline specifically:

- **Immutable CI inputs.** All GitHub Actions and tool binaries are pinned to exact versions or SHA commit hashes, not mutable version tags. Go dependencies are integrity-checked via `go.sum` checksums. The Dockerfile used to build the embedded firewall and control-plane infrastructure image (`Dockerfile.controlplane`) is pinned to a SHA256 digest of its base image, so an attacker cannot substitute a tampered base image by re-tagging upstream.
- **Pinned BPF toolchain.** The BPF object files are produced by `clang`+`llvm-strip` from the Ubuntu 24.04 (Noble) apt repos, pinned to exact versions in the Makefile's `BPF_APT_DEPS` variable (see the Makefile for current pins). CI's `sudo make bpf-deps` step (on the pinned `ubuntu-24.04` runner) and the macOS-developer `Dockerfile.controlplane` (which builds on a SHA256-pinned `ubuntu:24.04` base) install from the same list — the Makefile is the single source of truth, so version bumps update both paths atomically.
- **Deterministic build inputs.** Binaries are compiled with `CGO_ENABLED=0` (pure Go, no C toolchain), stripped (`-s -w`), and built with `-trimpath` via GoReleaser. Build metadata (version, commit date) is injected via ldflags at compile time from the tagged commit, not from wall-clock or host environment. Byte-identical reproduction across hosts is not a claimed property — the BPF bytecode is produced by a pinned-but-not-snapshotted apt closure on the pinned `ubuntu-24.04` runner; the integrity story rests on signed attestations against the CI build, not local re-runs.
- **SHA256 checksums.** GoReleaser generates a `checksums.txt` containing SHA256 hashes for every release artifact.
- **Cosign keyless signing.** The checksums file is signed using [Sigstore Cosign](https://docs.sigstore.dev/) in keyless mode (OIDC identity from the GitHub Actions workflow). This produces a Sigstore bundle (`checksums.txt.sigstore.json`) published as a release asset. The signing identity anchors to the **reusable workflow** `release-build.yml` at the release tag, not the caller `release.yml` — see [Verifying a release](#verifying-a-release) for the exact identity regex.
- **SBOM generation.** A Software Bill of Materials is generated for every release archive using [Syft](https://github.com/anchore/syft) (SPDX-JSON format) and published as a release asset alongside the archives.
- **GitHub artifact attestation.** One SLSA v1 build-provenance attestation is produced per release via `actions/attest@v4`, covering **sixteen subjects**: the four release archives, the four primary `clawker` CLI binaries (linux/darwin × amd64/arm64), and the eight embedded Linux binaries (`clawkerd`, `clawker-cp`, `ebpf-manager`, `coredns-clawker` × `amd64`, `arm64`). The predicate is auto-generated by the action from GitHub Actions context — source commit, workflow ref, builder ID, runner environment. That source-commit binding transitively pins every reproducibility input in the repo: `Makefile` (`BPF_APT_DEPS`), `Dockerfile.controlplane`, `.goreleaser.yaml`, `internal/controlplane/firewall/ebpf/gen.go` (clang `-cflags`, bpf2go pin), BPF C sources, and the workflow YAMLs (with their pinned action SHAs). Anyone with a verified attestation can `git checkout` and inspect every input that affected output bytes. Same-repo SLSA Build L3 isolation is provided by GitHub's reusable-workflow boundary: signing happens in the reusable workflow's isolated job context, so the Fulcio cert SAN cryptographically binds to `release-build.yml@refs/tags/<tag>` and cannot be impersonated by any other workflow in this repo. Browse attestations at [github.com/schmitthub/clawker/attestations](https://github.com/schmitthub/clawker/attestations).
- **Tag integrity.** The release pipeline's tag trigger is protected by a GitHub repository ruleset that restricts creation, update, and deletion of release tags to repository administrators and requires cryptographic signing. The workflow additionally validates that release tags match a valid semver pattern and that the tagged commit is an ancestor of `main` — rejecting malformed version tags, tags on feature branches, or tags on arbitrary commits. Together, these controls ensure that only authorized, signed release tags with valid version syntax pointing to reviewed code on the main branch can trigger a release.
- **Immutable releases.** The tag ruleset prevents release tags from being deleted, moved, or force-pushed. Once a release is published, its artifacts, checksums, and attestations are frozen — a compromised CI pipeline cannot silently replace a previously published binary.
- **Branch rulesets.** All commits require cryptographic signing on every commit. Branches can not be deleted. The main branch requires all merges to pass CI status checks (security scanning, linting, unit tests, license checks, generated docs freshness) and be approved by a code owner. 

<Note>
There is an inherent risk in using third-party packages and libraries during software development, especially recently. Clawker itself, Claude Code, and anything you put on your host machine or in your container could become a victim of complex supply chain attacks. Developing in containers does help keep your system safer, but it does not eliminate the risk.

For example, if you had been using trivy or litellm in a container with restricted egress...

**trivy.** The stolen data is encrypted using a hybrid AES-256-CBC + RSA scheme and bundled into a tpcp.tar.gz archive, then exfiltrated via HTTP POST to a typosquatted domain scan.aquasecurtiy[.]org.

**litellm.** Stolen data is decoded then encrypted using AES-256-CBC with a randomly generated session key. That session key is subsequently encrypted using a hard-coded RSA public key embedded in the payload. The encrypted data and key are packaged into an archive (tpcp.tar.gz) and exfiltrated to a remote endpoint controlled by the attacker, including the domains models[.]litellm[.]cloud and checkmarx[.]zone. 

Regardless of how or if your container became infected, or what obfuscation and packaging they used... the exfiltration would have failed and cleaning the infection would involve simply building a new image with patched packages, and creating a fresh container.  
</Note>

### Verifying a release

**1. Download and checksum.** Download a release archive and verify its SHA256 checksum against the published `checksums.txt`:

```bash
mkdir -p /tmp/verify && gh release download v0.7.6 --repo schmitthub/clawker \
  --pattern 'clawker_0.7.6_linux_arm64.tar.gz' \
  --pattern 'checksums.txt*' --dir /tmp/verify

cd /tmp/verify && sha256sum -c checksums.txt --ignore-missing
# clawker_0.7.6_linux_arm64.tar.gz: OK
```

**2. Verify cosign signature on checksums.** This proves the checksums file was signed inside the reusable build workflow at the exact tagged commit — not by a human, a compromised process, or any other workflow in this repo. Requires [cosign](https://docs.sigstore.dev/cosign/system_config/installation/) v3+.

```bash
cosign verify-blob \
  --bundle /tmp/verify/checksums.txt.sigstore.json \
  --new-bundle-format \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github\.com/schmitthub/clawker/\.github/workflows/release-build\.yml@refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._-]+)?(\+[A-Za-z0-9._-]+)?$' \
  /tmp/verify/checksums.txt
# Verified OK
```

The regex anchors to the **reusable** workflow `release-build.yml` (where the cosign sign step runs), not the caller `release.yml`. A substring-only regex like `'github\.com/schmitthub/clawker'` would also match a forged attestation produced by any other workflow file in this repo. `--new-bundle-format` is required for bundles emitted by `actions/attest@v4` (`mediaType: application/vnd.dev.sigstore.bundle.v0.3+json`); cosign v3 defaults to the legacy format and rejects v0.3 silently.

**3. Verify SLSA build provenance attestation.** GitHub's verifier auto-fetches the signed bundle from the attestation store. Requires `gh` >= 2.49.

```bash
gh attestation verify /tmp/verify/clawker_0.7.6_linux_arm64.tar.gz \
  --owner schmitthub \
  --signer-workflow schmitthub/clawker/.github/workflows/release-build.yml
```

`gh attestation verify` returns no stdout on success — only an exit code. Add `--format json | jq` to surface the subject digest, builder ID, and signing identity.

### Per-binary forensic verification

The SLSA build-provenance attestation covers **sixteen subjects** — the four release archives, the four `clawker` CLI binaries, and the eight embedded Linux binaries — so an investigator can verify any individual binary without re-downloading the archive. This matters when:

- A long-running container holds a `clawker-cp` or `clawkerd` binary unpacked from a release months ago, and you want to confirm it has not been swapped under you.
- You have only one of the four embedded Linux binaries from a forensic image and need to answer "which release did this come from, and is it intact?"
- You are doing differential analysis after an incident and need to know which specific binary (if any) diverged from what was attested at build time.

The procedure is the same `gh attestation verify` call against the suspect binary — gh resolves the attestation by the binary's sha256 digest. The signing identity is the reusable workflow at the release tag; the subject set inside the signed predicate must contain that binary's `sha256` digest or verification fails.

To enumerate the attested inputs from the predicate alone (source commit, workflow ref, builder ID), dump the statement:

```bash
gh attestation verify /tmp/verify/clawker_0.7.6_linux_arm64.tar.gz --owner schmitthub \
  --signer-workflow schmitthub/clawker/.github/workflows/release-build.yml \
  --format json | jq '.[].verificationResult.statement.predicate.buildDefinition'
```

Every reproducibility input lives in the source tree at the attested commit — `Makefile` for the BPF apt pins, `internal/controlplane/firewall/ebpf/gen.go` for the bpf2go pin and clang flags, the workflow YAMLs for action SHAs. Resolve them via `git show <commit>:<path>` once you have the source commit from the attestation.

## How the Firewall Works

The firewall is central to most of clawker's threat mitigations, so it's worth understanding the mechanism before diving into specific threats.

All clawker containers run on a dedicated `clawker-net` bridge network, isolated from other Docker workloads and the host's network. Only internal traffic is freely allowed, but Docker's built-in DNS service discovery only resolves clawker's own internal services (Envoy, CoreDNS, the control plane, the host proxy via `host.docker.internal`, and the optional monitoring stack). The network gateway — which routes to the host machine — is locked down separately: only the host proxy port is allowed through, and all other traffic to the gateway is redirected to Envoy and denied. This prevents the agent from reaching host services even though it has network-level access to the bridge subnet.

Claude Code runs as an unprivileged `claude` user inside the container (UID baked at build time — on Linux hosts derived from the CLI invoker's `os.Getuid()` so the `~/.claude/projects/` bind mount is writable, 1001 elsewhere). The container's PID 1 is `clawkerd`, the per-container supervisor — it runs as root but never executes user-controlled code in its own process. Privilege drop happens kernel-side between fork and exec via `SysProcAttr.Credential`, so there is no user-mode code path that briefly holds root before dropping it. eBPF cgroup programs that enforce the firewall are attached to the container's cgroup from outside the container by the clawker control plane — agent containers themselves run fully unprivileged (`cap_add: []`, no `NET_ADMIN`, no `NET_RAW`).

Per-container enforcement is keyed by presence in a global `container_map` BPF map. The TCP routing table (`route_map`) is itself global and keyed by `{domain_hash, dst_port}` — containers that are enrolled in `container_map` see enforcement, everything else passes through. Rule changes are pushed live via an `ebpf-manager sync-routes` call whenever `clawker firewall add/remove/reload` runs, so the data plane reflects new rules without restarting containers or the firewall stack.

Per-domain TCP routing depends on a `dns_cache` BPF map that tracks `ip → domain_hash` for every A-record answer. Clawker ships a **custom CoreDNS build** with an in-tree `dnsbpf` plugin that writes to this map in real time on every successful DNS response. This replaces an earlier startup-time DNS seeding approach — seeding was vulnerable to DNS round-robin (CDN backends rotate faster than the firewall reloads), which could silently blackhole traffic to allowlisted domains. With the plugin, whatever IP the resolver hands the container has already been observed and hashed at the moment of connection. Only NOERROR responses with A records populate the map; NXDOMAIN and other failures are passed through untouched so blocked domains never leak in. The CoreDNS container runs with `CAP_BPF` + `CAP_SYS_ADMIN` and a `/sys/fs/bpf` bind mount — the minimum privileges required to open the pinned map and update it.

This layered approach means traffic is controlled at the DNS level (unlisted domains never resolve), the TCP level (Envoy enforces SNI matching, protocol rules, and path-level restrictions), the UDP level (non-DNS UDP is blocked entirely), and the ICMP level (all outbound ICMP is dropped to prevent ICMP tunneling).

**IPv6 scope.** The eBPF `cgroup/connect6` program applies the full IPv4 routing pipeline (DNS redirect, gateway lockdown, per-domain route lookup, Envoy fallback) to **IPv4-mapped IPv6 addresses** (`::ffff:x.x.x.x`). This matters because most modern programs — `ssh`, `curl`, Node.js, Python — open dual-stack IPv6 sockets by default and connect to IPv4 targets via IPv4-mapped addresses. A prior implementation allowed all IPv4-mapped addresses through without enforcement, which was a silent dual-stack bypass for nearly every common tool; that gap is closed. Native IPv6 (connections to real IPv6 addresses such as `2606:4700::1`, or traffic forced via `-6`) is **denied** — the egress listeners, DNS forward zones, and BPF maps are IPv4-only, and transparent native-IPv6 routing is not supported today.

**When the firewall is toggled off** for a container (via `clawker firewall disable`), clawker updates the container's DNS routing to Docker's embedded DNS (`127.0.0.11`), which forwards externally to Cloudflare's anti-malware DNS nameservers (`1.1.1.2`, `1.0.0.2`). This provides a safety net — the agent gets unrestricted network access, but DNS still filters known-malicious domains. Re-enabling the firewall restores the routing to Envoy and CoreDNS.

Envoy and CoreDNS emit structured access logs (domain, protocol, HTTP path, response codes, DNS query details) that flow into OpenSearch alongside agent telemetry. (see: [Monitoring](/monitoring))

See the [Firewall](/firewall) guide for the full architecture, configuration, and CLI reference.

<Note>
Claude Code's `WebSearch` tool [uses Anthropic's API to perform searches](https://platform.claude.com/docs/en/agents-and-tools/tool-use/web-search-tool#how-web-search-works). This means the agent may appear to reach domains that are not in your allowlist via search results, but this is not a firewall bypass. The `WebFetch` tool, however, does go through the container's network and is subject to the firewall.
</Note>

### Infrastructure container privileges

Enforcing the agent-container boundary requires a small number of clawker-managed infrastructure containers that run with elevated permissions:

- **Control plane (`clawker-controlplane`)** holds `CAP_SYS_ADMIN` + `CAP_BPF`, RW-mounts `/sys/fs/bpf`, RO-mounts `/sys/fs/cgroup`, bind-mounts the Docker socket, and runs with `apparmor=unconfined`. It owns eBPF program loading, cgroup attach/detach, and lifecycle of the sibling firewall containers. See [Control Plane → Container Privileges](/control-plane#container-privileges) for the per-privilege rationale.
- **CoreDNS (`clawker-coredns`)** runs with `CAP_BPF + CAP_SYS_ADMIN` and a `/sys/fs/bpf` bind mount — the minimum required to write resolved `IP → domain_hash` entries into the pinned `dns_cache` map at every successful DNS reply. See [Firewall → Architecture](/firewall#architecture).
- **Envoy (`envoyproxy/envoy`)** runs unprivileged; it is the data-plane TLS terminator only.

These privileges sit on the *enforcement* side of the trust boundary and are **not extended to your agent containers**. Agent containers run unprivileged (`cap_add: []`, docker-default seccomp + AppArmor, no kernel-surface mounts, no Docker socket). The infrastructure containers' job is to constrain what agent containers can do — they do not give the agent any additional reach. Anything that can tamper with the CP binary, the embedded `ebpf-manager`, or the bind-mounted CA can subvert enforcement for every agent on the host, which is why the [release verification](#verifying-a-release) procedure matters for upgrades.

## Data Exfiltration via Network Egress

**Threat:** A prompt-injected or hallucinating agent sends source code, secrets, environment variables, or credentials to an attacker-controlled endpoint — or leaks sensitive data to unintended services.

**Attack surface:** Any outbound network connection from the container.

**How clawker mitigates it:**

- **Deny-by-default firewall.** The Envoy+CoreDNS firewall blocks all outbound traffic. TCP is routed to Envoy via eBPF cgroup/connect4 for IPv4 and cgroup/connect6 for IPv4-mapped dual-stack IPv6, DNS is routed to CoreDNS, and non-DNS UDP is dropped entirely. Native IPv6 is denied outright (see IPv6 scope in [How the Firewall Works](#how-the-firewall-works)). Traffic to the `clawker-net` CIDR is allowed (for Envoy, CoreDNS, and the optional monitoring stack), but everything else is deny-by-default. Only explicitly whitelisted domains on specific protocols are reachable. DNS queries for unlisted domains return NXDOMAIN — the domain never even resolves. When a known (cached) domain is reached on a port that has no matching rule, the connection falls through to Envoy's egress listener where the deny chain resets it; earlier bypass paths through dual-stack IPv6 were closed as part of the `cgroup/connect6` hardening.
- **Domain-based enforcement.** Domains are whitelisted by name, not by IP address. This is critical — many domains sit behind shared CDN edges like Cloudflare, so IP-whitelisting one domain could accidentally open the entire internet. IPs also rotate frequently in load-balanced environments, making IP-based rules fragile. The proxy honors the requested domain but performs its own DNS resolution from the allowlist — the agent cannot influence where traffic actually goes. Connections to unlisted domains are rejected. A coerced agent that spoofs a whitelisted domain in its request headers while targeting an attacker-controlled IP still ends up at the real whitelisted service — the attacker's IP is never contacted.
- **Path-level filtering.** For domains that need fine-grained control, the proxy uses protocol inspection to enforce restrictions down to the HTTP path. You can allow a domain but restrict which endpoints are reachable — useful for scoping access on platforms that mix trusted and untrusted content (e.g., allow `api.github.com/repos/` but block `api.github.com/gists`).
- **Minimal hardcoded allowlist.** Only the domains required for Claude Code itself to function (API, OAuth, telemetry) are allowed by default. See the [Security](/security#hardcoded-allowed-domains) page for the canonical list. GitHub, npm, PyPI, and every other service must be explicitly added. 
<Warning>
If docker support is enabled in your project's configuration, Docker image pulls are handled by the host daemon, outside the container's network namespace, and are not subject to the container firewall. See [Docker Socket](#docker-socket) in the Shared Responsibility section for the implications of this and how to manage it.
</Warning>
- **DNS exfiltration prevention.** DNS-based exfiltration is a well-known technique where stolen data is encoded into DNS queries — for example, resolving `stolen-secret.attacker.com` or encoding data as subdomains (`dG9rZW4.exfil.attacker.com`). This is often effective because DNS traffic is overlooked by network controls and even a single query can leak data. When the firewall is enabled, Clawker routes all DNS requests through the managed CoreDNS instance, which only has forward zones for whitelisted domains. Every other query — including crafted exfiltration subdomains — returns `NXDOMAIN`. There is no upstream resolver fallback (_specific internal service names, like those for the monitoring stack, forward to Docker embedded DNS, meaning arbitrary service discovery is unavailable when the firewall is enabled_). NXDOMAIN responses are never written to the eBPF `dns_cache` map, so a blocked domain cannot smuggle an attacker-controlled IP into the per-domain route table. When the firewall is disabled, the container resolves to Docker's embedded DNS, which forwards to [Cloudflare's anti-malware DNS nameservers](https://developers.cloudflare.com/1.1.1.1/setup/#1111-for-families) (`1.1.1.2`, `1.0.0.2`). 
- **Raw TCP port-level routing.** TLS and HTTP carry domain metadata (SNI, Host header) that Envoy can inspect, allowing multiple domains per port. Raw TCP protocols like SSH have no such field — there's nothing in the stream identifying the intended domain. IP-based rules are not viable because IPs rotate frequently behind CDNs and load balancers. Instead, clawker redirects all traffic on a given port to the whitelisted domain for that port. A `proto: ssh` rule for `github.com` on port 22 captures every outbound port 22 connection and routes it to GitHub — an attacker cannot reach an arbitrary SSH server. If multiple rules target the same port, the first rule wins. Multi-domain support for raw TCP ports is a [tracked limitation](https://github.com/schmitthub/clawker/issues/235). **If you are pentesting clawker's firewall:** a successful TCP connect on a port with a whitelisted rule does **not** mean you reached your intended target. The connection was silently redirected to the whitelisted service. Verify by checking the remote banner or certificate before concluding you have egress.
- **ICMP blocked.** ICMP tunneling tools (e.g., `ptunnel`, `icmpsh`) can exfiltrate data by encoding it in ICMP echo request/reply payloads. While slow (~50-100 KB/s), this is effective because ICMP is neither TCP nor UDP and is often overlooked by network controls. Clawker blocks raw socket creation via eBPF cgroup/sock_create, preventing ICMP tunneling entirely.

See the [Firewall](/firewall) guide for the full architecture, configuration, and CLI commands.

## Host Infection / Filesystem Damage 

**Threat:** A hallucinating or coerced agent runs destructive commands (`rm -rf`, overwrites, corrupted writes), installs malicious or unapproved packages, accesses sensitive files on the host system — personal documents, SSH keys, system configuration, other projects. 

**Attack surface:** The host filesystem.

**How clawker mitigates it:**

- **Container isolation.** The agent runs inside a Docker container. The host filesystem is not mounted into the container by default — the agent has no awareness of or access to host system files, user files, tools, or other projects. They simply don't exist from the agent's perspective.
- **Controlled workspace mounting.** The only host files the agent can touch are the ones the user explicitly mounts — either as a live bind mount or an ephemeral snapshot copy. Snapshot mode creates a disposable copy in a Docker volume, so changes never reach the host at all. In bind mode, the blast radius is limited to the mounted directory.
- **Version-controlled repositories.** For git repositories, the damage from destructive file operations is largely mitigated by version control — the user can always reset, checkout, or recover from the git history.
- **Unprivileged user.** The agent runs as a non-root user (`claude:claude`) inside the container. It cannot escalate privileges, modify system files within the container, or bypass file permission boundaries on mounted directories.
- **No host resource access.** System tools, package managers, shell configuration, browser profiles, credential stores, and everything else on the host are outside the container boundary. The agent cannot install system-level packages, modify host shell config, or access other applications.
- **User-controlled secrets.** Any secrets the container has access to (environment variables, env files, forwarded credentials) are explicitly provided by the user — and their exfiltration is mitigated by the network firewall.

See [Container Internals](/container-internals) for bind vs snapshot details and workspace configuration.

<Warning>
**`.clawkerignore` and tmpfs overlays.** 

Directories listed in `.clawkerignore` (e.g. `node_modules/`, `vendor/`, `.venv/`) are masked inside the container with empty tmpfs mounts mounted as `root` (macOS doesn't support UID/GID settings) when in `workspace.bind` mode (`snapshot ignores copying them`). This prevents binary and dependency cross-contamination between host and container operating systems. Because of this the tmpfs mounts use the `exec` setting, overriding Docker's default `noexec`, but `nosuid` and `nodev` are on by default.

The `noexec` hardening pattern targets unattended ephemeral production containers where the entire root filesystem is read-only and tmpfs is the attacker's only writable surface — restricting `exec` on `/tmp` and `/dev/shm` blocks the classic "download and execute" post-compromise chain. That threat model does not apply to a development container. In our case, the workspace bind mount already has `exec`. An attacker who can drop a binary to `node_modules/` can just as easily drop it to the project root or `src/`. So `noexec` on the tmpfs overlays is a speed bump, not a barrier. The real controls are the egress firewall (prevents downloading payloads), the unprivileged user (prevents privilege escalation), and seccomp filtering (restricts dangerous syscalls).
</Warning>

## Summary

| Threat | Primary Control | Default | Guide |
|--------|----------------|:---:|-------|
| Data exfiltration (network + DNS) | Network firewall + CoreDNS NXDOMAIN | On | [Firewall](/firewall) |
| Data exfiltration (ICMP tunneling) | eBPF sock_create blocks SOCK_RAW | On | [Firewall](/firewall) |
| Host filesystem damage | Container isolation + workspace mount scoping | On | [Container Internals](/container-internals) |

## Shared Responsibility

Clawker provides the guardrails, but your configuration choices determine your actual security posture. The controls above are only as strong as how you configure them.

### Firewall Management

The firewall is your primary defense against data exfiltration. Every domain you add to the allowlist is attack surface you're accepting. Be deliberate:

- **Avoid untrusted domains or domains with untrusted content.** Storage buckets, UGC platforms, and gist-style services can host attacker-controlled content that your agent fetches — including prompt injection payloads. Use specific subdomains instead of top-level domains where possible, and leverage path rules to exclude endpoints that serve user-generated or untrusted content.
- **Don't disable the firewall** unless you're in a trusted, isolated environment and accept fully unrestricted egress.

### Credential Scoping

The credentials you inject via environment variables or env files are available to the agent for the entire session. Apply least privilege:

- Scope tokens to the minimum permissions needed. If the agent only reads repositories, use a `read`-scoped `GITHUB_TOKEN`, not `repo:write`.
- Avoid injecting long-lived secrets when short-lived or session-scoped tokens are available.
- Review what your `agent.env` and `agent.env_file` entries expose before each session.

### Docker Socket

Enabling `security.docker_socket: true` mounts the host Docker socket into the container. The Docker CLI inside the container is the real Docker CLI talking directly to your host Docker daemon — not Docker-in-Docker (which is resource-heavy and brittle). This means the agent has full, unfiltered control over your Docker daemon and can create, modify, and delete any container, image, or volume on your host.

<Danger>
**Mounting the Docker socket is a major privilege escalation vector.** The agent gains root-equivalent access to the host — it can mount arbitrary host paths into new containers, read any file on the host filesystem, spawn privileged containers, and escape the isolation that clawker otherwise provides. This effectively negates container isolation as a security boundary.

Docker-outside-of-Docker (DooD) was chosen over Docker-in-Docker (DinD) for performance and reliability — DinD requires a nested daemon, consumes significantly more resources, and introduces complex storage driver issues. However, alternative approaches that provide Docker capabilities without full socket access are being evaluated for future releases.

**If you enable this setting, treat the session as supervised.** Do not leave an agent running unattended with socket access. Review what containers and volumes the agent creates. Disable `security.docker_socket` when Docker access is not actively needed.
</Danger>

<Warning>clawker's own resource jailing (`pkg/whail`) is a UX feature for the *user*, not a security boundary for the agent. It prevents you from accidentally running `clawker volume prune` and deleting non-clawker volumes. But the agent inside the container uses the real Docker CLI, not clawker — so the jailing doesn't apply. Only enable the Docker socket if your workflow genuinely requires container-driven development (e.g., building and running containers as part of your CI or dev workflow). Otherwise, leave it off.</Warning>

### Privileged Mode

Clawker does **not** run containers in privileged mode by default. The `--privileged` flag is available on `clawker container create` and `clawker container exec` for users who explicitly need it, but it is off unless you ask for it.

<Danger>
**Running containers with `--privileged` grants elevated permissions and can expose the host system to security risks.** Privileged mode broadly relaxes confinement — seccomp filtering and AppArmor profiles are typically disabled, capabilities like `CAP_SYS_ADMIN` are added, and device access controls are opened up. While the container still runs in its own namespaces, these permissions allow it to effectively bypass isolation (for example by mounting host filesystems) and interact with the host kernel much like a root process on the host. Use this option only when absolutely necessary and in trusted environments.

Clawker's security controls (unprivileged user, nosuid mounts, seccomp, capability dropping) are effectively neutralized in privileged mode. If you pass `--privileged`, you are accepting full responsibility for what the agent does.
</Danger>

### Bypass Discipline

The bypass exists to avoid firewall rule management fatigue. Sometimes the agent just needs to do some research or install a package and you don't want to add permanent rules for a one-off domain. That's what bypass is for — short, supervised windows of unrestricted access.

Use small time windows (e.g., `clawker firewall bypass 30s --agent dev`). Pay attention while it's active. If you notice anything suspicious, immediately close the bypass with `clawker firewall bypass --stop --agent <agent-name>`. Bypass is scoped to a specific agent — it only toggles the egress restrictions for that one container. The firewall stack itself is not turned off, and all other agents continue to have their traffic routed through it normally. This limits exposure to a single container rather than your entire fleet. That said, the bypassed container has fully unrestricted network access for the duration, so don't leave long bypasses running unattended.

### Workspace Scope

Be intentional about what you bind-mount:

- Don't mount directories containing `.env` files, private keys, or credentials the agent doesn't need.
- Use `.clawkerignore` to mask sensitive directories.
- Use snapshot mode for experimental or high-risk agent tasks where you don't want any changes reaching the host.

### Base Image Trust

Custom build instructions (`build.instructions` in your project config) run during image build. Injected `root_run` commands execute as root. Review what you're adding — package sources, custom scripts, and copied files are part of your supply chain.

### Resource Limits

Clawker containers are throwaway devcontainers, not production workloads. Docker handles OOM kills natively, but you should configure your Docker engine's default resource limits to prevent a runaway agent from starving the host. Use `--memory` and `--cpus` flags on container creation for per-agent limits.
