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 bubblewrapordnf install bubblewrap. Version 0.10 or later. - - Seatbelt (macOS): built into macOS as
sandbox-exec. - - gVisor (containers):
runscruntime. Install per gVisor docs. - -
seccomp(Linux): kernel feature; profiles via systemdSystemCallFilter=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.servicecan call this instead ofclaudedirectly. - -
seccomp-claude.json, illustrative seccomp profile in OCI format. Usestrace -cagainst 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 withsandbox-exec -D WORKSPACE="$PWD" -D HOME="$HOME" -f claude.sb /usr/local/bin/claude. - -
gvisor-runtimeclass-and-pod.yaml, Kubernetes RuntimeClass declaringrunscand 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-parentand--new-session. - -
--unshare-netplus a forgotten--bind /run/network-namespacethat re-exposes networking. - - Seccomp profile that allows
clonewithout flag filtering. Agents spawn unsandboxed children viaclone(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 0 | PR.PS-01, PR.PS-05, PR.PS-06, PR.IR-01 |
|---|---|
| NIST AI RMF | MANAGE 2.4 |
| OWASP LLM | LLM05, LLM10 |
| OWASP AGENTIC | ASI02, ASI05 |
| OTHER | NIST SP 800-160 Vol. 1 |
Cite this cell:
https://agenticcovenants.com/protect/blast-radius/client-side/