Agentic Covenants

Protect (PR) · Blast radius

Blast radius at the client side layer

deterministic · Outside the model's reasoning

If the agent decides to violate this concern, what stops it at this layer?

What this cell does

Sandbox at launch (Seatbelt, bubblewrap, gVisor), seccomp/AppArmor, --network none, read-only mounts, dry-run defaults.

Artifacts (4)

agent-bwrapview on GitHub
#!/usr/bin/env bash
# ABOUTME: Bubblewrap launcher that sandboxes the agent with inheritance enforcement and no direct network.
# ABOUTME: Network access goes through /run/agent-egress.sock, an out-of-sandbox proxy that owns the allowlist.

set -euo pipefail

WORK_DIR="${1:-$PWD}"
shift || true

if [[ ! -d "$WORK_DIR" ]]; then
  echo "agent-bwrap: workspace dir does not exist: $WORK_DIR" >&2
  exit 64
fi

EGRESS_SOCK="${EGRESS_SOCK:-/run/agent-egress.sock}"
SECCOMP_FILE="${SECCOMP_FILE:-/etc/agents/seccomp-claude.json}"
USE_NETWORK="${USE_NETWORK:-1}"

BWRAP_FLAGS=(
  --ro-bind /usr /usr
  --ro-bind /lib /lib
  --ro-bind /lib64 /lib64
  --ro-bind /etc /etc
  --tmpfs /tmp
  --tmpfs /var/tmp
  --bind "$WORK_DIR" /workspace
  --chdir /workspace
  --proc /proc
  --dev /dev
  --unshare-uts
  --unshare-ipc
  --unshare-pid
  --new-session
  --die-with-parent
  --cap-drop ALL
)

if [[ "$USE_NETWORK" == "0" ]]; then
  BWRAP_FLAGS+=( --unshare-net )
else
  # Network through the egress proxy only. The proxy owns the allowlist and
  # lives outside the sandbox; a compromised agent cannot rewrite it.
  if [[ ! -S "$EGRESS_SOCK" ]]; then
    echo "agent-bwrap: egress socket missing: $EGRESS_SOCK" >&2
    echo "Start the egress proxy or run with USE_NETWORK=0 for offline tasks." >&2
    exit 1
  fi
  BWRAP_FLAGS+=(
    --unshare-net
    --bind "$EGRESS_SOCK" "$EGRESS_SOCK"
    --setenv HTTPS_PROXY "unix://${EGRESS_SOCK}"
    --setenv HTTP_PROXY "unix://${EGRESS_SOCK}"
  )
fi

# Apply seccomp if the kernel and bubblewrap support --seccomp.
# (Some distros' bubblewrap is built without seccomp support; we degrade
# gracefully rather than fail the launch.)
if [[ -r "$SECCOMP_FILE" ]] && bwrap --help 2>&1 | grep -q -- '--seccomp'; then
  # bubblewrap takes a binary BPF program on a file descriptor. Translate the
  # OCI seccomp JSON into BPF using your runtime's helper, or skip and rely on
  # systemd-side SystemCallFilter= in the unit file.
  :
fi

exec bwrap "${BWRAP_FLAGS[@]}" /usr/local/bin/claude "$@"
claude.sbview on GitHub
;; ABOUTME: macOS Seatbelt sandbox profile for the agent. Run with sandbox-exec -D WORKSPACE=$PWD -D HOME=$HOME -f claude.sb.
;; ABOUTME: Network egress is denied except through a unix-domain-socket proxy at /private/var/run/agent-egress.sock.

(version 1)
(deny default)

;; Process management
(allow process-fork)
(allow process-exec
  (literal "/usr/local/bin/claude")
  (subpath "/usr/bin")
  (subpath "/bin")
  (subpath "/usr/local/bin"))

(allow signal (target self))

;; Read-only system reads
(allow file-read*
  (subpath "/usr")
  (subpath "/Library/Frameworks")
  (subpath "/System/Library")
  (subpath "/private/etc")
  (literal "/etc")
  (literal "/dev/random")
  (literal "/dev/urandom")
  (literal "/dev/null"))

;; Workspace is read-write. Substituted at launch time via -D WORKSPACE=...
(allow file-read* file-write*
  (subpath (param "WORKSPACE")))

;; Agent runtime config in the operator's home dir is read-only. The
;; credential file is owned by root, ACLed away from the agent's user.
(allow file-read*
  (subpath (param "HOME") "/.claude"))

;; tmpfs equivalent for transient files
(allow file-read* file-write*
  (subpath "/private/tmp")
  (subpath "/private/var/tmp"))

;; IPC for system services the agent must call
(allow mach-lookup
  (global-name "com.apple.system.notification_center")
  (global-name "com.apple.system.opendirectoryd.api"))

;; Network: deny everything by default, allow only the egress proxy socket.
(deny network*)
(allow network-outbound
  (literal "/private/var/run/agent-egress.sock"))

;; Resource limits applied at sandbox layer (also enforced by launchd)
(allow sysctl-read)
gvisor-runtimeclass-and-pod.yamlview on GitHub
# ABOUTME: gVisor RuntimeClass and an example Pod using runsc for syscall-level isolation in Kubernetes.
# ABOUTME: Pair with the projected-token Pod in controls/identity/server-side/. Substitute the image digest.
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc
---
apiVersion: v1
kind: Pod
metadata:
  name: claude-agent
  namespace: agent-claude-prod
  labels:
    app: claude-code
spec:
  runtimeClassName: gvisor
  serviceAccountName: claude-code
  automountServiceAccountToken: false
  securityContext:
    runAsNonRoot: true
    runAsUser: 1000
    runAsGroup: 1000
    fsGroup: 1000
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: claude
    # Pin by digest. Resolve current digest with: crane digest registry.example.com/claude-agent:v1
    image: registry.example.com/claude-agent@sha256:REPLACE_WITH_DIGEST_FROM_CRANE
    securityContext:
      readOnlyRootFilesystem: true
      runAsNonRoot: true
      runAsUser: 1000
      allowPrivilegeEscalation: false
      capabilities:
        drop: ["ALL"]
      seccompProfile:
        type: RuntimeDefault
    resources:
      requests:
        cpu: 100m
        memory: 256Mi
      limits:
        cpu: 1
        memory: 1Gi
        ephemeral-storage: 1Gi
    volumeMounts:
    - name: agent-token
      mountPath: /var/run/secrets/agents
      readOnly: true
    - name: workdir
      mountPath: /workspace
    - name: tmp
      mountPath: /tmp
  volumes:
  - name: agent-token
    projected:
      sources:
      - serviceAccountToken:
          path: token
          expirationSeconds: 900
          audience: agent-claude-prod
  - name: workdir
    emptyDir:
      sizeLimit: 500Mi
  - name: tmp
    emptyDir:
      sizeLimit: 100Mi
seccomp-claude.jsonview on GitHub
{
  "_comment": "Illustrative seccomp profile for Claude Code. Derive your real allowlist with: strace -c -f /usr/local/bin/claude --some-real-task. Anthropic publishes a reference seccomp profile in their sandbox docs; consult the current list.",
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "archMap": [
    {
      "architecture": "SCMP_ARCH_X86_64",
      "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"]
    },
    {
      "architecture": "SCMP_ARCH_AARCH64",
      "subArchitectures": ["SCMP_ARCH_ARM"]
    }
  ],
  "syscalls": [
    {
      "names": [
        "accept", "accept4", "access", "alarm", "bind", "brk", "chdir",
        "chmod", "chown", "clock_getres", "clock_gettime", "clock_nanosleep",
        "close", "connect", "creat", "dup", "dup2", "dup3", "epoll_create1",
        "epoll_ctl", "epoll_wait", "eventfd2", "execve", "exit", "exit_group",
        "faccessat", "fadvise64", "fallocate", "fchdir", "fchmod", "fchmodat",
        "fchown", "fchownat", "fcntl", "fdatasync", "flock", "fstat",
        "fstatfs", "fsync", "ftruncate", "futex", "getcwd", "getdents",
        "getdents64", "getegid", "geteuid", "getgid", "getgroups", "getpeername",
        "getpgid", "getpgrp", "getpid", "getppid", "getpriority", "getrandom",
        "getresgid", "getresuid", "getrlimit", "getrusage", "getsid",
        "getsockname", "getsockopt", "gettid", "gettimeofday", "getuid",
        "getxattr", "ioctl", "ioprio_get", "ioprio_set", "kill", "lchown",
        "link", "linkat", "listen", "lseek", "lstat", "madvise", "memfd_create",
        "mkdir", "mkdirat", "mmap", "mprotect", "mremap", "munmap", "nanosleep",
        "newfstatat", "open", "openat", "pause", "pipe", "pipe2", "poll",
        "ppoll", "prctl", "pread64", "preadv", "preadv2", "prlimit64",
        "pselect6", "pwrite64", "pwritev", "pwritev2", "read", "readahead",
        "readlink", "readlinkat", "readv", "recvfrom", "recvmmsg", "recvmsg",
        "rename", "renameat", "renameat2", "rt_sigaction", "rt_sigpending",
        "rt_sigprocmask", "rt_sigqueueinfo", "rt_sigreturn", "rt_sigsuspend",
        "rt_sigtimedwait", "rt_tgsigqueueinfo", "sched_getaffinity",
        "sched_getparam", "sched_getscheduler", "sched_yield", "select",
        "sendfile", "sendmmsg", "sendmsg", "sendto", "setfsgid", "setfsuid",
        "setgid", "setgroups", "setitimer", "setpgid", "setpriority",
        "setregid", "setresgid", "setresuid", "setreuid", "setrlimit",
        "setsid", "setsockopt", "setuid", "shutdown", "signalfd4", "socket",
        "socketpair", "splice", "stat", "statfs", "statx", "symlink",
        "symlinkat", "sync", "sync_file_range", "syncfs", "sysinfo", "tee",
        "tgkill", "time", "timer_create", "timer_delete", "timer_getoverrun",
        "timer_gettime", "timer_settime", "timerfd_create", "timerfd_gettime",
        "timerfd_settime", "times", "tkill", "truncate", "umask", "uname",
        "unlink", "unlinkat", "utime", "utimensat", "utimes", "vfork", "wait4",
        "waitid", "write", "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    },
    {
      "_comment": "clone is allowed but with flag filtering; the agent must not create new namespaces.",
      "names": ["clone", "clone3"],
      "action": "SCMP_ACT_ALLOW",
      "args": [
        {
          "index": 0,
          "value": 2114060288,
          "valueTwo": 0,
          "op": "SCMP_CMP_MASKED_EQ"
        }
      ]
    },
    {
      "_comment": "Forbidden by omission: ptrace, mount, umount2, unshare, setns, kexec_load, init_module, finit_module, delete_module, reboot, swapon, swapoff, etc.",
      "names": [],
      "action": "SCMP_ACT_ERRNO"
    }
  ]
}

Cell notes

Blast radius / Client-side

Control. Sandbox at process launch with inheritance enforcement. Linux: bubblewrap. macOS: Seatbelt. Containerized: gVisor. Network isolation via unix-domain-socket egress proxy. Seccomp or AppArmor profile applied at launch. --network none for non-network tasks. Read-only volume mounts. Dry-run defaults.

Strength. Deterministic for the syscall and filesystem surface covered by the profile. Bypass through unsandboxed children when inheritance is not enforced, profile gaps, kernel-level escape (rare), or read-only mounts that are read-only at the bind but writable elsewhere on the same FS via a different mountpoint.

Tooling

  • - bubblewrap (Linux): apt-get install bubblewrap or dnf install bubblewrap. Version 0.10 or later.
  • - Seatbelt (macOS): built into macOS as sandbox-exec.
  • - gVisor (containers): runsc runtime. Install per gVisor docs.
  • - seccomp (Linux): kernel feature; profiles via systemd SystemCallFilter= or container runtime --security-opt seccomp=.
  • - AppArmor (Linux): kernel LSM; profiles in /etc/apparmor.d/.

Files in this directory

  • - agent-bwrap, bubblewrap launcher script. Drop in /usr/local/bin/. Wraps the agent in a sandbox with inheritance enforcement (--die-with-parent, --new-session), no network by default, capability dropping. The systemd unit in ../../identity/client-side/claude-code-prod.service can call this instead of claude directly.
  • - seccomp-claude.json, illustrative seccomp profile in OCI format. Use strace -c against your real workload to derive the actual minimal allowlist. Anthropic publishes a reference seccomp profile for Claude Code in their sandbox docs; consult the current list.
  • - claude.sb, Seatbelt sandbox profile for macOS. Run with sandbox-exec -D WORKSPACE="$PWD" -D HOME="$HOME" -f claude.sb /usr/local/bin/claude.
  • - gvisor-runtimeclass-and-pod.yaml, Kubernetes RuntimeClass declaring runsc and a sample Pod that uses it. Combines with the Pod from ../../identity/server-side/pod-with-projected-token.yaml.

Verification


# 1. Sandbox is active: a privileged read fails
agent-bwrap /tmp -- /bin/sh -c 'cat /etc/shadow'
# expected: failure (file not bound or not readable)

# 2. Network is blocked
agent-bwrap /tmp -- curl -sS https://example.com
# expected: connection refused or no network

# 3. Child inheritance is enforced
agent-bwrap /tmp -- /bin/sh -c 'sh -c "curl -sS https://example.com"'
# expected: also fails

# 4. Seccomp profile is loaded (test inside a container)
docker run --rm --security-opt seccomp=/etc/agents/seccomp-claude.json \
  alpine /bin/sh -c 'unshare -n /bin/sh'
# expected: Operation not permitted if unshare not in allowlist

# 5. gVisor runtime is in effect
kubectl exec claude-agent -- dmesg | head
# expected: runsc-specific output ("Starting gVisor...")

Common mistakes

  • - Sandbox set on the parent only; child processes inherit nothing. Use --die-with-parent and --new-session.
  • - --unshare-net plus a forgotten --bind /run/network-namespace that re-exposes networking.
  • - Seccomp profile that allows clone without flag filtering. Agents spawn unsandboxed children via clone(CLONE_NEWNS).
  • - AppArmor profile in complain mode (logs but does not enforce). Confirm with aa-status.
  • - Read-only mounts that are read-only at the bind but writable elsewhere on the same FS via a different mountpoint.
  • - Egress proxy allowlist living inside the sandbox. The allowlist must live outside the sandbox so a compromised agent cannot rewrite it.

Citation

NIST CSF 2.0 PR.PS-01, PR.PS-05, PR.PS-06 (secure software development practices), PR.IR-01 (networks protected). NIST AI RMF MANAGE 2.4. OWASP LLM05, LLM10 (Unbounded Consumption). OWASP ASI05 (Unexpected Code Execution). NIST SP 800-160 Vol. 1 (defense in depth).

Primary bypasses

Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.

  • unsandboxed children when inheritance not enforced
  • profile gaps
  • kernel-level escape

Crosswalk

NIST CSF 2 0PR.PS-01, PR.PS-05, PR.PS-06, PR.IR-01
NIST AI RMFMANAGE 2.4
OWASP LLMLLM05, LLM10
OWASP AGENTICASI02, ASI05
OTHERNIST SP 800-160 Vol. 1