npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@puku-ai/sandbox-runtime

v0.2.7

Published

Puku Sandbox Runtime (PSRT) - A general-purpose tool for wrapping security boundaries around arbitrary processes

Readme

Puku Sandbox Runtime (PSRT)

A small tool that locks down what a process can do — what files it can read or write, and which websites it can reach — using the security features already built into your operating system. No containers, no virtual machines, no background daemons.

It works on macOS (sandbox-exec + Seatbelt), Linux (bubblewrap + seccomp), and Windows (puku-win + WFP + NTFS ACLs). A built-in HTTP/SOCKS5 proxy on the host machine checks every outbound request against your rules.

Puku Sandbox Runtime is the layer underneath Puku CLI: every shell command the agent runs is wrapped in a sandbox on your machine, so it can only do what your policy allows. The same building blocks are published as a standalone package, so any TypeScript program can wrap its own untrusted subprocesses.

What PSRT is and is not.

PSRT is not a VM, a container, or a daemon. There is no background service to start, no image to build, no special kernel module to load — it just wires together the sandboxing primitives your OS already ships. Your current policy lives in ~/.srt-settings.json (puku-srt init writes a default). Policy is per-user; per-project overrides live in .puku/sandbox.json in the working directory.

Installation

npm install -g @puku-ai/sandbox-runtime

This puts two binaries on your $PATH:

  • srt — kept as a synonym for backwards compatibility with existing scripts.
  • puku-srt — the Puku-branded CLI. Same engine, Puku-style messages.

⚠️ Before you run anything — install Linux host prerequisites.

The Linux sandbox needs three binaries that come from your OS package manager, not from npm. If they are missing the CLI will print apt install socat (or bwrap / ripgrep) and refuse to start:

# Debian/Ubuntu
sudo apt install bubblewrap socat ripgrep
# Fedora/RHEL
sudo dnf install bubblewrap socat ripgrep
# Arch
sudo pacman -S bubblewrap socat ripgrep

Why these are NOT bundled: bubblewrap is a setuid-root helper that must be installed by the distro (it needs to match the host kernel and libc), socat is a general-purpose userspace network tool, and ripgrep is the canonical deny-path scanner. The npm tarball cannot ship working binaries for them. (See Per-platform dependencies below for the full contract and the Ubuntu 24.04+ AppArmor workaround.)

macOS and Windows need no host packages — macOS uses kernel sandbox-exec natively, Windows uses the bundled puku-win.exe helper.

Basic usage

# Network restrictions
$ puku-srt "curl api.github.com"
Running: curl api.github.com
<html>...</html>                                              # allowed

$ puku-srt "curl example.com"
Running: curl example.com
Connection blocked by network allowlist                       # blocked

# Filesystem restrictions
$ puku-srt "cat README.md"
Running: cat README.md
# Puku Sandb...                                                # current dir is allowed

$ puku-srt "cat ~/.ssh/id_rsa"
Running: cat ~/.ssh/id_rsa
cat: /Users/ollie/.ssh/id_rsa: Operation not permitted        # blocked

Overview

This package gives you a sandbox you can use two ways: as a CLI, or as a library from TypeScript. The defaults are safe — processes start with almost nothing, and you open only the holes you need.

Key features:

  • Network rules — pick which hosts/domains can be reached over HTTP/HTTPS and other protocols
  • Filesystem rules — pick which files and directories can be read or written
  • Unix socket rules — restrict local IPC sockets
  • Violation monitoring — on macOS, watch the system sandbox log in real time

Example: sandboxing an MCP server

A common use is wrapping a Model Context Protocol (MCP) server so it can only do what you allow. To sandbox the filesystem MCP server:

Before (.mcp.json):

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem"]
    }
  }
}

After (.mcp.json):

{
  "mcpServers": {
    "filesystem": {
      "command": "puku-srt",
      "args": ["npx", "-y", "@modelcontextprotocol/server-filesystem"]
    }
  }
}

Then write the rules to ~/.srt-settings.json:

{
  "filesystem": {
    "denyRead": [],
    "allowWrite": ["."],
    "denyWrite": ["~/sensitive-folder"]
  },
  "network": {
    "allowedDomains": [],
    "deniedDomains": []
  }
}

Now the MCP server can't write to the denied folder:

> Write a file to ~/sensitive-folder
✗ Error: EPERM: operation not permitted, open '/Users/ollie/sensitive-folder/test.txt'

How it works

PSRT uses the OS's own sandbox primitives. The restrictions apply to the whole process tree (children, grandchildren, anything the process spawns).

  • macOS: sandbox-exec with a dynamically generated Seatbelt profile
  • Linux: bubblewrap for container-style isolation plus seccomp for syscall filtering
  • Windows: the wrapped process runs as a dedicated puku-sandbox local user, fenced by Windows Filtering Platform rules keyed on its SID and by NTFS ACLs on the paths you choose

Two layers, both required

You need both filesystem and network isolation. With only files, a compromised process can post your SSH keys somewhere. With only network, it can grab unrestricted internet access.

Filesystem uses two different rules:

  • Read (deny-then-allow): by default, reading is allowed everywhere. You deny broad regions (e.g. /Users) and re-allow specific paths inside them (e.g. .). allowRead wins over denyRead.
  • Write (allow-only): by default, writing is denied everywhere. You must allow specific paths (e.g. ., /tmp). An empty allow list means no writes at all.

Network is allow-only: by default, all network access is denied. You name the domains that are allowed (empty list = no network). All traffic goes through a proxy on the host:

  • Linux — requests travel over a Unix domain socket. The sandboxed process has no network namespace, so it can only reach the host's proxy sockets (bind-mounted into the sandbox).
  • macOS — only a specific localhost port is reachable from inside the sandbox; the proxy listens there.
  • Windows — a machine-wide WFP filter blocks all outbound connects from the puku-sandbox account except loopback to the proxy port range.

Both HTTP/HTTPS (through the HTTP proxy) and raw TCP (through the SOCKS5 proxy) are gated by your domain rules.

Read more about sandboxing in Puku CLI:

End-to-end flow: from puku-cli to a sandboxed command

These diagrams show how PSRT fits into a real puku-cli run. They are executable in prose — every arrow corresponds to a real call path you can grep for in the source.

1. Component layout (where PSRT sits in the stack).

┌──────────────────────────────────────────────────────────────────────┐
│  User terminal                                                       │
│  └─ $ puku-cli "Bash(curl https://api.github.com)"                  │
└───────────────────────────────┬──────────────────────────────────────┘
                                │ stdin / argv
                                ▼
┌──────────────────────────────────────────────────────────────────────┐
│  puku-cli (Bun bundle, dist/cli.mjs)                                │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  BashTool → permissions check → tool dispatch                  │  │
│  └───────────────────────────────┬────────────────────────────────┘  │
│                                  │ sandboxConfig + command          │
│                                  ▼                                   │
│  ┌────────────────────────────────────────────────────────────────┐  │
│  │  src/utils/sandbox/sandbox-adapter-v2.ts                      │  │
│  │  getSandboxManager().wrap(...)                                │  │
│  │  (PukuJail.checkDependencies() on Linux)                      │  │
│  └───────────────────────────────┬────────────────────────────────┘  │
└──────────────────────────────────┼───────────────────────────────────┘
                                   │ @puku-ai/sandbox-runtime (PSRT)
                                   ▼
┌──────────────────────────────────────────────────────────────────────┐
│  PukuJail (PSRT singleton)                                           │
│  ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐  │
│  │  macOS sandbox   │ │  Linux sandbox   │ │  Windows sandbox     │  │
│  │  sandbox-exec +  │ │  bwrap + seccomp │ │  puku-win.exe + WFP  │  │
│  │  Seatbelt        │ │  (delegates to   │ │  + NTFS ACLs         │  │
│  │                  │ │  puku-linux.ts)  │ │                      │  │
│  └──────────────────┘ └──────────────────┘ └──────────────────────┘  │
└───────────────────────────────┬──────────────────────────────────────┘
                                │ wrapped spawn (argv preserved)
                                ▼
┌──────────────────────────────────────────────────────────────────────┐
│  Sandboxed child process (e.g. `curl …`)                              │
│  ─ file I/O filtered by FS rules (bwrap bind-mounts / seccomp /      │
│    Seatbelt / WFP)                                                   │
│  ─ network only via host proxy (Unix socket / localhost / WFP loop)  │
└──────────────────────────────────────────────────────────────────────┘

2. Request lifecycle — puku-cli "Bash(curl https://api.github.com)".

  USER         puku-cli          PSRT (PukuJail)       bwrap+seccomp       child curl
   │              │                    │                    │                  │
   │  submit msg  │                    │                    │                  │
   │─────────────►│                    │                    │                  │
   │              │ wrap(cmd, policy)  │                    │                  │
   │              │───────────────────►│                    │                  │
   │              │                    │ checkDependencies()│                  │
   │              │                    │ (bwrap, socat, rg) │                  │
   │              │                    │ ──────────────────►│                  │
   │              │                    │                    │                  │
   │              │                    │ build Seatbelt /   │                  │
   │              │                    │ bwrap --args /     │                  │
   │              │                    │ puku-win.exe       │                  │
   │              │                    │ config            │                  │
   │              │                    │─────────┐          │                  │
   │              │                    │         ▼          │                  │
   │              │                    │ start proxy on     │                  │
   │              │                    │ host (HTTP+SOCKS5) │                  │
   │              │                    │─────────┐          │                  │
   │              │                    │         ▼          │                  │
   │              │                    │ socat UNIX-LISTEN  │                  │
   │              │                    │  → TCP:localhost   │                  │
   │              │                    │ (Linux only)       │                  │
   │              │                    │─────────┐          │                  │
   │              │                    │         ▼          │                  │
   │              │                    │  spawn  ◄──────────── bind-mount unix sock
   │              │                    │ ────────│──────────│─────────────►    │
   │              │                    │         │          │ execve(curl)     │
   │              │                    │         │          │                  │
   │              │                    │         │          │ GET /            │
   │              │                    │         │          │ api.github.com   │
   │              │                    │         │          │─────────────────►│
   │              │                    │         │          │                  │
   │              │                    │         │          │ (only allowed    │
   │              │                    │         │          │  via unix sock)  │
   │              │                    │         ▼          │◄─────────────────│
   │              │                    │ host proxy checks  │                  │
   │              │                    │ domain allowlist   │                  │
   │              │                    │ (allowed → forward │                  │
   │              │                    │  blocked → TCP     │                  │
   │              │                    │  RST into sandbox) │                  │
   │              │                    │◄───────────────────│                  │
   │              │                    │                   │                  │
   │  ← render    │ stream events back │ exit code /        │                  │
   │    result    │◄───────────────────│  violation log     │                  │
   │              │                    │                    │                  │

3. Linux network bridge — why socat is needed.

The sandboxed child has no network namespace of its own. It can only reach the host's 127.0.0.1 loopback (and even that is restricted by the Seatbelt / WFP profile). To route its HTTP/SOCKS5 traffic through the host proxy, PSRT sets up a socat bridge between a Unix domain socket (visible inside the sandbox) and a localhost TCP port where the proxy listens:

                       ┌──────────────────────────────┐
                       │  INSIDE bwrap sandbox        │
                       │                              │
                       │  curl ──► /tmp/srt-http.sock │
                       │       (AF_UNIX, bind-mounted │
                       │        into sandbox)         │
                       └──────────────┬───────────────┘
                                      │  socat bridges:
                                      │  UNIX-LISTEN → TCP:127.0.0.1:N
                                      ▼
   ┌──────────────────────────────────────────────────────────────┐
   │  HOST (outside sandbox)                                      │
   │                                                              │
   │  socat ──► 127.0.0.1:N ──► HTTP proxy ──► domain allowlist   │
   │                                            ▼                 │
   │                                     allow → real network     │
   │                                     deny  → TCP RST          │
   └──────────────────────────────────────────────────────────────┘

This is why socat is in the host prerequisites list — without it, the bridge has no implementation on Linux. On macOS the same effect is achieved through Seatbelt's network rules + a localhost listener. On Windows a machine-wide WFP filter keys on the puku-sandbox account's SID and only allows loopback to the proxy port range.

Architecture

src/
├── index.ts                       # Public exports (Puku* names)
├── cli.ts                         # Legacy `srt` CLI shim (calls puku/cli.ts as 'srt')
└── puku/                          # ★ single Puku-native source tree
    ├── cli.ts                     # `puku-srt` CLI entrypoint
    ├── jail/                      # PukuJail (lifecycle + wrap)
    ├── policy/                    # PukuPolicy Zod schemas
    ├── proxy/                     # HTTP / SOCKS / mux / MITM TLS termination
    ├── credentials/               # credential-masking pipeline
    ├── linux/                     # Linux backend (bwrap + seccomp + socat)
    ├── darwin/                    # macOS backend (sandbox-exec + Seatbelt)
    ├── win/                       # Windows backend (puku-win + WFP + NTFS ACL)
    └── util/                      # path / shell-quote / ripgrep / debug helpers

Usage

As a CLI

puku-srt wraps any command with the sandbox. srt works as a synonym.

# Run a command in the sandbox
puku-srt echo "hello world"

# With debug logging
puku-srt --debug curl https://example.com

# Use a custom settings file
puku-srt --settings /path/to/srt-settings.json npm install

As a library

import { PukuJail, type PukuPolicy } from '@puku-ai/sandbox-runtime'
import { spawn } from 'child_process'

// 1. Write your policy
const config: PukuPolicy = {
  network: {
    allowedDomains: ['example.com', 'api.github.com'],
    deniedDomains: [],
  },
  filesystem: {
    denyRead: ['~/.ssh'],
    allowWrite: ['.', '/tmp'],
    denyWrite: ['.env'],
  },
}

// 2. Initialize the sandbox (starts proxy servers, etc.)
await PukuJail.initialize(config)

// 3. Wrap a command
const sandboxedCommand = await PukuJail.wrapWithSandbox(
  'curl https://example.com',
)

// 4. Run it
const child = spawn(sandboxedCommand, { shell: true, stdio: 'inherit' })

child.on('exit', async code => {
  console.log(`Command exited with code ${code}`)
  await PukuJail.reset() // optional; also runs on process exit
})

Tagging violations with commandId / commandText. Violations seen while a wrapped command runs (seatbelt log lines, seccomp events, proxy denies) are stored under an attribution key. By default that key is the wrapped string itself. Pass an opaque per-invocation commandId (for example a tool-use id) to key by that instead — recommended, because keys compare on the first 100 characters, so long commands sharing a prefix would otherwise cross-attribute, and a rerun of the same text would inherit the earlier run's events.

If the string you execute isn't the command the invocation represents (e.g. you wrap an assembled source <snapshot> && eval '<cmd>'), also pass commandText: '<cmd>' — that's what ignoreViolations command patterns match against, and what each violation reports as its command.

const wrapped = await PukuJail.wrapWithSandbox(
  assembledCommand, // what actually runs
  undefined,
  undefined,
  undefined,
  { commandId: invocationId, commandText: rawCommand },
)
// ... run it ...
const annotated = PukuJail.annotateStderrWithSandboxFailures(
  invocationId,
  stderr,
)

Public exports

// Facade
export { PukuJail } from '@puku-ai/sandbox-runtime'

// Violation tracking
export { PukuViolationLog } from '@puku-ai/sandbox-runtime'

// Types
export type {
  PukuPolicy, // top-level config
  PukuNetworkConfig,
  PukuFilesystemConfig,
  PukuCredentialsConfig,
  PukuIgnoreViolationsConfig,
  PukuAskCallback, // host-asks-user callback
  PukuFsReadConfig, // filesystem read restriction config
  PukuFsWriteConfig, // filesystem write restriction config
  PukuNetworkAllowConfig, // per-host allow rules
  PukuHostPattern,
  PukuRequestFilter, // per-request decision hook
  PukuViolationEvent,
} from '@puku-ai/sandbox-runtime'

The pre-Phase-9 Sandbox* aliases (SandboxManager, SandboxViolationStore, SandboxRuntimeConfig) were dropped in 0.1.0 — import the Puku* names directly. As of 0.1.0, the public surface is:

export { PukuJail } from '@puku-ai/sandbox-runtime'
export type { WrapWithPukuOptions } from '@puku-ai/sandbox-runtime'
export { PukuViolationLog } from '@puku-ai/sandbox-runtime'
export type { PukuPolicy } from '@puku-ai/sandbox-runtime'
// … plus Puku* platform backends / proxy helpers / credential pipeline

Configuration

Where the settings file lives

By default PSRT reads ~/.srt-settings.json. Override with --settings:

srt --settings /path/to/srt-settings.json <command>

Full example

{
  "network": {
    "allowedDomains": [
      "github.com",
      "*.github.com",
      "lfs.github.com",
      "api.github.com",
      "npmjs.org",
      "*.npmjs.org"
    ],
    "deniedDomains": ["malicious.com"],
    "allowUnixSockets": ["/var/run/docker.sock"],
    "allowLocalBinding": false
  },
  "filesystem": {
    "denyRead": ["~/.ssh"],
    "allowRead": [],
    "allowWrite": [".", "src/", "test/", "/tmp"],
    "denyWrite": [".env", "config/production.json"]
  },
  "ignoreViolations": {
    "*": ["/usr/bin", "/System"],
    "git push": ["/usr/bin/nc"],
    "npm": ["/private/tmp"]
  },
  "enableWeakerNestedSandbox": false,
  "enableWeakerNetworkIsolation": false,
  "allowAppleEvents": false
}

Field reference

Network

Network is allow-only — everything is denied by default.

  • network.allowedDomains — domains the sandbox may reach. Supports wildcards (*.example.com). Empty array = no network at all. An optional :port suffix (api.example.com:443, *.example.com:8443) restricts the entry to that destination port; entries without a port match any port.
    • IPv6 literals must be bracketed, RFC 3986-style: [::1], [2001:db8::1]:443. An unbracketed multi-colon entry is rejected as ambiguous (2001:db8::1:443 is itself a valid address).
  • network.deniedDomains — domains to block. Checked before allowedDomains, so denials win. Same :port syntax; a bare * (or *:22) means deny everything.
  • network.deniedDomainReasons — optional map from a deniedDomains entry (matched by exact string) to a human-readable reason shown in the <sandbox_violations> line when that entry denies a connection — say what's blocked and the sanctioned alternative (e.g. {"github.com:22": "SSH pushes to GitHub are blocked; use an https:// remote"}). For SSH destinations (port 22), the reason also gets delivered in-band: an SSH client tunnelled through a no-auth SOCKS ProxyCommand (e.g. BSD nc -X 5) receives a pre-key-exchange SSH disconnect whose description is the reason, which OpenSSH prints verbatim — keep such reasons under ~400 ASCII characters, imperative first, since OpenSSH truncates and escapes non-ASCII.
  • network.allowLocalBinding — allow binding to local ports (boolean, default false).

TLS termination (network.tlsTerminate, experimental): when set, HTTPS CONNECTs are terminated in-process so PSRT can see (and filter, via network.filterRequest) the decrypted requests. The sandboxed process is pointed at a trust bundle containing the MITM CA (caCertPath/caKeyPath, or an ephemeral CA if omitted) plus the host's regular roots, so proxy-minted certs and real upstream certs both verify.

  • network.tlsTerminate.excludeDomains — domain patterns (same syntax as allowedDomains) that are not terminated. Matching CONNECTs are tunnelled opaquely instead: they're still subject to the domain allowlist, but the client inside the sandbox completes its own TLS handshake with the real upstream, and filterRequest / credential injection don't apply to their HTTPS traffic. Use this for the two cases TLS termination fundamentally breaks:
    • mTLS upstreams — only the in-sandbox client holds the client certificate, so the proxy can't re-originate the connection.
    • Certificate-pinning clients — clients that verify the upstream's identity themselves (custom CAs, SAN pinning) and reject the MITM certificate.
  • network.tlsTerminate.extraCaCertPaths — paths to PEM CA certificate files appended to the trust bundle, after the MITM CA and the host's regular roots. Excluded (non-terminated) hosts are verified by the client inside the sandbox, and the trust env vars PSRT sets (SSL_CERT_FILE, GIT_SSL_CAINFO, …) replace each tool's own trust configuration, so a site-local root (e.g. an internal mTLS CA) must be in the bundle or those hosts can never be verified. Only the CERTIFICATE blocks of each file are copied into the bundle (anything else — e.g. a private key in a combined PEM — is never exposed to the sandbox); missing or unreadable files are skipped, so it's safe to list paths that exist on only some hosts.
{
  "network": {
    "allowedDomains": ["*.example.com", "internal-mtls.example.net"],
    "deniedDomains": [],
    "tlsTerminate": {
      "excludeDomains": ["internal-mtls.example.net"],
      "extraCaCertPaths": ["/etc/internal-mtls-roots.pem"]
    }
  }
}

Unix socket settings (platform-specific):

| Setting | macOS | Linux | | ------------------------------ | ------------------------- | ---------------------------------------- | | allowUnixSockets: string[] | Allowlist of socket paths | Ignored (seccomp can't filter by path) | | allowAllUnixSockets: boolean | Allow all sockets | Disable seccomp blocking |

Unix sockets are blocked by default on both platforms.

  • macOS: use allowUnixSockets to allow specific paths (e.g. ["/var/run/docker.sock"]), or allowAllUnixSockets: true to allow all.
  • Linux: blocking uses seccomp filters (x64/arm64 only). If seccomp isn't available, sockets are unrestricted and a warning is shown. Use allowAllUnixSockets: true to explicitly disable blocking.

Filesystem

Two different rule patterns.

Read (deny-then-allow) — all reads allowed by default:

  • filesystem.denyRead — paths to block. Empty = full read access.
  • filesystem.allowRead — paths to re-allow inside denied regions (takes precedence over denyRead). Note: this is the opposite of write, where denyWrite wins over allowWrite.

Write (allow-only) — all writes denied by default:

  • filesystem.allowWrite — paths the sandbox may write. Empty = no writes at all.
  • filesystem.denyWrite — paths to block inside allowed regions (takes precedence over allowWrite).

Path syntax on macOS:

Paths support git-style globs on macOS, like .gitignore:

  • * — any chars except / (*.ts matches foo.ts, not foo/bar.ts)
  • ** — any chars including / (src/**/*.ts matches all .ts files under src/)
  • ? — any single char except / (file?.txt matches file1.txt)
  • [abc] — any char in the set (file[0-9].txt matches file3.txt)

Examples:

  • "allowWrite": ["src/"] — allow writing to src/
  • "allowWrite": ["src/**/*.ts"] — allow writing to all .ts files under src/
  • "denyRead": ["~/.ssh"] — block reading the SSH folder
  • "denyRead": ["/Users"], "allowRead": ["."] — block reading all of /Users, re-allow the current directory
  • "denyWrite": [".env"] — block writing .env (even if . is allowed)

Path syntax on Linux:

Linux does not support globs. Use literal paths:

  • "allowWrite": ["src/"] — allow writing to src/
  • "denyRead": ["/home/user/.ssh"] — block reading the SSH folder
  • "denyRead": ["/home"], "allowRead": ["."] — block reading all of /home, re-allow the current directory

All platforms:

  • Paths can be absolute (/home/user/.ssh) or relative to the current working directory (./src).
  • ~ expands to your home directory.

Other options

  • ignoreViolations — maps command patterns to paths where violations should be silently dropped
  • enableWeakerNestedSandbox — enable weaker sandbox mode for Docker environments (boolean, default false)
  • javaAgentJarPath — macOS/Linux: absolute path to srt-proxy-agent.jar, the JVM agent injected via JAVA_TOOL_OPTIONS (see "JVM tools" below). Only needed by consumers that bundle sandbox-runtime and ship the jar separately; a normal npm install finds it under vendor/java-proxy-agent/.
  • enableWeakerNetworkIsolation — allow access to com.apple.trustd.agent in the macOS sandbox (boolean, default false). Needed for Go programs (gh, gcloud, terraform, kubectl, …) to verify TLS certificates when using httpProxyPort with a MITM proxy and a custom CA. Security warning: enabling this opens a potential data-exfiltration path through the trustd service.
  • allowAppleEvents — allow sending Apple Events and Launch Services open requests from the macOS sandbox (boolean, default false). Without this, commands like open, osascript, and anything that opens URLs or scripts other apps via AppleScript fail with AppleScript error -600 ("Application isn't running") or LaunchServices errors (-10822, -54). Security warning: enabling this means the sandbox no longer provides code-execution isolation — a sandboxed command can launch other applications via open with no user prompt, and anything it launches runs outside the sandbox's filesystem and network restrictions; scripting already-running apps via Apple Events is additionally gated by the user's per-app TCC automation consent. Embedders should only source this option from trusted user-level configuration — never from project-local files in a checked-out repository, which would let an attacker-authored project elevate its own sandbox permissions.

Common recipes

Allow GitHub access (all the endpoints you actually need):

{
  "network": {
    "allowedDomains": [
      "github.com",
      "*.github.com",
      "lfs.github.com",
      "api.github.com"
    ],
    "deniedDomains": []
  },
  "filesystem": {
    "denyRead": [],
    "allowWrite": ["."],
    "denyWrite": []
  }
}

Restrict to specific directories:

{
  "network": {
    "allowedDomains": [],
    "deniedDomains": []
  },
  "filesystem": {
    "denyRead": ["~/.ssh"],
    "allowWrite": [".", "src/", "test/"],
    "denyWrite": [".env", "secrets/"]
  }
}

Workspace-only filesystem access (deny reads outside the workspace):

{
  "network": {
    "allowedDomains": [],
    "deniedDomains": []
  },
  "filesystem": {
    "denyRead": ["/Users"],
    "allowRead": ["."],
    "allowWrite": ["."],
    "denyWrite": []
  }
}

This blocks reading anything under /Users (or /home on Linux), then re-allows the current working directory. System paths (/usr, /lib, …) remain readable.

Common issues

Running Jest: pass --no-watchman to avoid sandbox violations:

srt "jest --no-watchman"

Watchman touches files outside the sandbox boundaries and trips permission errors. Disabling it makes Jest use its built-in file watcher instead.

Platform support

  • macOS — uses sandbox-exec with custom Seatbelt profiles (no extra dependencies)
  • Linux — uses bubblewrap (bwrap) for containerisation
  • Windows — alpha. Uses a bundled puku-win.exe helper (no extra dependencies). See Windows (alpha) below for setup, security model, and known limitations.

Per-platform dependencies

Bundled vs. host: the npm package ships three platform-specific helpers (apply-seccomp for Linux x86-64/arm64, puku-win.exe for Windows x64/arm64, srt-proxy-agent.jar for macOS+Linux JVMs). Everything else listed below — Linux's bubblewrap, socat, and ripgrep — comes from the OS package manager, not from npm and not from the install hook. They are required on Linux because they have to match the host kernel/libc exactly and must be installed with the distro's setuid/file-cap conventions.

| Linux sandbox backend | Host prerequisite (NOT bundled) | apt install line (REQUIRED on Linux) | | --------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------- | | bubblewrap (bwrap) | setuid-root helper that creates the user-namespace container | apt install bubblewrap socat ripgrep | | socat | userspace UNIX-LISTEN → TCP:localhost:port proxy bridge | (same line as above) | | ripgrep (rg) | fast deny-path scanner | (same line as above) | | apply-seccomp (vendored) | seccomp-BPF filter installer (loaded by bwrap --arg-file) | n/a — ships inside the npm tarball |

Linux requires:

  • bubblewrap — container runtime
    • Ubuntu/Debian: apt-get install bubblewrap
    • Fedora: dnf install bubblewrap
    • Arch: pacman -S bubblewrap
  • socat — socket relay for proxy bridging
    • Ubuntu/Debian: apt-get install socat
    • Fedora: dnf install socat
    • Arch: pacman -S socat
  • ripgrep — fast search tool for deny path detection
    • Ubuntu/Debian: apt-get install ripgrep
    • Fedora: dnf install ripgrep
    • Arch: pacman -S ripgrep

Ubuntu 24.04+ note: these releases enable kernel.apparmor_restrict_unprivileged_userns by default, which allows unshare(CLONE_NEWUSER) but strips capabilities from the resulting namespace. Both bubblewrap and the seccomp isolation layer need capability-bearing user namespaces. Disable the restriction with:

sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

…or add an AppArmor profile that grants userns to the relevant binaries.

Optional Linux dependencies (for seccomp fallback):

The package ships pre-generated seccomp BPF filters for x86-64 and arm. You'll only need these tools on architectures where pre-generated filters aren't available:

  • gcc or clang — C compiler
  • libseccomp-dev — seccomp library dev files
    • Ubuntu/Debian: apt-get install gcc libseccomp-dev
    • Fedora: dnf install gcc libseccomp-devel
    • Arch: pacman -S gcc libseccomp

macOS requires:

  • ripgrep — fast search tool for deny path detection
    • Homebrew: brew install ripgrep
    • Or grab a release: https://github.com/BurntSushi/ripgrep/releases

Windows requires:

  • No additional dependencies. The puku-win.exe helper (x64 and arm64) ships with the npm package. Run a one-time elevated windows-install step (see below).

Windows (alpha)

Windows support is alpha. The wrapped process runs under a dedicated puku-sandbox local user account, isolated from the calling user by native Windows security primitives: a Windows Filtering Platform (WFP) egress fence keyed on the sandbox account's SID, and per-session explicit ACEs on the configured filesystem paths.

Setup

Run once per machine (self-elevates; one UAC prompt):

npx @puku-ai/sandbox-runtime windows-install

This provisions the puku-sandbox local user account (random password stored DPAPI-encrypted in HKLM\SOFTWARE\puku-ai\sandbox-runtime — machine-wide, so fleet installs running as SYSTEM work and one user's rotation updates the copy the others read), the puku-sandbox-users local group, and installs a machine-wide WFP filter set keyed on the puku-sandbox SID. It is idempotent — re-running rotates the sandbox account's password and reconciles the filter set.

No logout is required. The WFP filters key on the dedicated sandbox account's SID, so your own network, services, and every other principal on the machine are unaffected.

After install, PukuJail.initialize() and puku-srt work like on the other platforms. initialize() verifies the sandbox account and WFP fence are live, and fails with an actionable error if they aren't.

Programmatic install/uninstall are exported as installPukuWin() / uninstallPukuWin().

Security model

The wrapped command runs as the puku-sandbox account, not as you. The bundled puku-win.exe does a two-hop launch: the broker calls CreateProcessWithLogonW to start a runner as puku-sandbox, and the runner spawns the target under a restricted token inside a job object. The child inherits the sandbox account's isolated profile (%USERPROFILE%, %TEMP%, HKCU) and a fresh environment overlaid with only the broker's PATH and the generated proxy variables.

Running under a distinct user SID structurally closes the surrogate- spawn class of escape (Task Scheduler, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS onto a broker-owned process, BITS, out-of-process COM with RunAs="Interactive User"): any process the child manages to spawn out-of-band still carries the puku-sandbox SID, so it stays subject to the WFP egress fence and has no rights on your files.

Network isolation is a two-filter WFP set at FWPM_LAYER_ALE_AUTH_CONNECT_V4/V6: a PERMIT for loopback destinations inside the configured proxy port range (default 60080–60089), and a BLOCK for any connect whose token carries the puku-sandbox SID. The wrapped process reaches the internet only via the JS HTTP/SOCKS5 proxies listening in that range; a process that strips its proxy environment and connects directly is blocked at the kernel.

Filesystem isolation is enforced by NTFS discretionary ACLs. The puku-sandbox account has no inherent rights on your files, so at initialize() the sandbox writes additive, inheriting explicit ACEs for the puku-sandbox SID only — it never rewrites or replaces a path's existing security descriptor:

  • filesystem.allowWrite → an inheriting MODIFY ALLOW ACE (READ|WRITE|EXECUTE|DELETE, with FILE_DELETE_CHILD withheld). The wrapped process can create, modify, and delete files inside the working tree; withholding FILE_DELETE_CHILD from the grant is defense-in-depth for the deny stamps below, not a guard on the tree root.
  • filesystem.allowRead → an inheriting READ|EXECUTE ALLOW ACE
  • filesystem.denyRead / filesystem.denyWrite → an inheriting DENY ACE on the target, plus an inheriting FILE_DELETE_CHILD DENY on its parent — together with the withheld FILE_DELETE_CHILD on the working-tree grant, this stops the wrapped process from renaming or deleting a denied path via its parent directory.

reset() removes every ACE this session added (refcounted across this user's concurrent hosts via the per-user session DB; a crash-recovery pass on the next initialize() cleans up after an unclean exit). Directory targets are supported (the ACEs inherit to the whole subtree). Glob patterns are expanded to concrete paths at initialize() time — a matching path that appears later is not covered.

TLS termination on Windows

network.tlsTerminate requires the MITM CA to be present in the sandbox user's CurrentUser\Root certificate store (schannel — the TLS backend used by System32\curl.exe, PowerShell Invoke-WebRequest, .NET, and default-backend git — trusts only the OS store, not environment variables). This is an install-time step, separate from windows-install:

import { pukuTrustCa } from '@puku-ai/sandbox-runtime'
pukuTrustCa('/path/to/mitm-ca.crt') // or: puku-win user trust-ca <path>

initialize() compares the session CA's thumbprint against the installed one and fails with an actionable message on mismatch, so a stale install-time CA can't silently break TLS inside the sandbox.

OpenSSL-backed clients (msys2 curl, git -c http.sslBackend=openssl, Node, Python, cargo) are covered by the env-var trust layer: the same trust bundle used on macOS/Linux is passed into the sandbox via NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, CURL_CA_BUNDLE, GIT_SSL_CAINFO, CARGO_HTTP_CAINFO, etc., and the bundle path is added to the session's allowRead grant so the sandbox account can open it.

Windows-specific configuration

The cross-platform filesystem and network blocks work as described above. Windows-only settings live under windows:

  • windows.proxyPortRange[low, high] inclusive port range the JS proxies bind inside. Must match the range passed to windows-install --proxy-port-range (default [60080, 60089]) — the WFP loopback PERMIT only covers that range.
  • windows.sublayerGuid — WFP sublayer GUID the filters were installed under. Omit to use the compile-time default; set only when enterprise tooling installed the filters under a custom sublayer.
  • windows.srtWin.path — path to the puku-win binary. Omit to resolve the packaged vendor/puku-win/<arch>/puku-win.exe. Set when embedding puku-win's CLI into a multicall binary; spawns then pass --puku-win as argv[1] so the embedder's dispatcher can route to puku_win::run_from_args.

Known limitations

  • Certificate revocation under schannel. CryptoAPI's CRL/OCSP fetch goes out via WinHTTP under the caller's token, ignoring the proxy environment, so it's blocked by the WFP egress fence. Tools that use schannel with revocation checking on by default fail with CRYPT_E_REVOCATION_OFFLINE (0x80092013) unless revocation is disabled per tool: curl --ssl-no-revoke, git -c http.schannelCheckRevoke=false, CARGO_HTTP_CHECK_REVOKE=false. Invoke-WebRequest, .NET HttpClient, and gh don't check revocation by default and are unaffected. A CRL distribution point served from the loopback proxy is planned to remove this workaround.
  • Per-user tool installs are not reachable. The wrapped process runs as puku-sandbox, not as you, so tools installed under your profile (nvm/fnm-managed Node, per-user winget/Scoop packages, pip install --user, %LOCALAPPDATA%\Programs\…) resolve on the inherited PATH but can't be opened by the sandbox account. Prefer machine-wide installs (Program Files, choco/winget --scope machine), or add the specific profile paths to filesystem.allowRead.
  • Per-exec filesystem.allowRead / filesystem.allowWrite overrides are not supported. Session-level allowRead/allowWrite (in the config passed to initialize()) work as described above; passing them per-command in wrapWithSandbox's customConfig throws — grants are applied session-wide via puku-win acl grant at initialize(), and puku-win exec only exposes per-exec denies.
  • proxyAuthToken is visible in the runner's command line. The proxy environment (including HTTP_PROXY=http://srt:<token>@127.0.0.1:…) is passed to the two-hop runner as --env arguments on puku-win exec's argv, so the token is readable by any local principal that can open the runner process for PROCESS_QUERY_LIMITED_INFORMATION. The token exists so the wrapped process can authenticate to the loopback proxy, so it isn't a secret from the sandbox itself; on a single- user development machine this is generally acceptable, but on a shared host treat the proxy allowlist as reachable by other same- session principals.
  • DNS resolution via the system resolver is not fenced. getaddrinfo() is serviced by the Dnscache service running as NETWORK SERVICE, so name resolution succeeds even though the subsequent connect() from the wrapped process is blocked. Tools that do their own UDP/53 (nslookup, dig) are fenced. This mirrors the macOS behaviour.

Uninstall

npx @puku-ai/sandbox-runtime windows-uninstall

Removes the WFP filter set, the puku-sandbox account and its profile, the puku-sandbox-users group, and removes the HKLM\SOFTWARE\puku-ai\sandbox-runtime key (credential, marker, CA record) — one UAC prompt. %ProgramData%\puku-ai\sandbox-runtime (the CA key material) is left in place; delete it (and %LOCALAPPDATA%\puku-ai\sandbox-runtime per user) manually for a full sweep.

Development

# Install dependencies
npm install

# Build the project
npm run build

# Run tests
npm test

# Type checking
npm run typecheck

# Lint code
npm run lint

# Format code
npm run format

Building seccomp binaries

The BPF filter and apply-seccomp loader are compiled from C source in vendor/seccomp-src/ via npm run build:seccomp (Linux only; needs gcc and libseccomp-dev). CI runs it before tests on each Linux arch, and the release workflow builds both arches and bundles them into the published package.

Implementation details

Network isolation architecture

PSRT runs HTTP and SOCKS5 proxy servers on the host that filter every network request against your rules:

  1. HTTP/HTTPS traffic — the HTTP proxy intercepts requests and validates them against allowed/denied domains
  2. Other TCP traffic — the SOCKS5 proxy handles everything else (SSH, database connections, …)
  3. Rule enforcement — the proxies enforce your domain rules

Per-platform proxy plumbing:

  • Linux — requests travel over Unix domain sockets (using socat to bridge). The network namespace is removed from the bubblewrap container, so all traffic must go through the proxies.
  • macOS — the Seatbelt profile allows communication only to specific localhost ports where the proxies listen. Everything else is blocked.
  • Windows — a WFP ALE_AUTH_CONNECT filter blocks every outbound connect from the puku-sandbox account except loopback to the configured proxy port range. The proxies bind inside that range. Environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, …) point tools at the proxies, but the WFP filter is the boundary — a process that ignores or unsets them is still fenced.

JVM tools (macOS/Linux): the JVM ignores HTTPS_PROXY/NO_PROXY and has no environment variable for proxy credentials — proxy selection comes from the https.proxyHost system properties and the credential can only be supplied through java.net.Authenticator. So JVM-based tools (Bazel's gRPC remote cache, Gradle, Maven, …) would otherwise dial the target directly and fail, or reach the proxy without its token and get a 407. To close that gap PSRT injects a small -javaagent via JAVA_TOOL_OPTIONS (the env var carries only the jar path, the credential stays in HTTPS_PROXY). At JVM start the agent sets http[s].proxyHost/Port and http.nonProxyHosts from the proxy env vars, re-enables Basic auth for CONNECT tunnels, and installs an Authenticator for the proxy endpoint. Explicit -D proxy properties on the JVM command line still win, and any inherited JAVA_TOOL_OPTIONS is preserved (unless it is a denied credential env var). Every JVM prints a Picked up JAVA_TOOL_OPTIONS: … line to stderr as a result; a jlink'd runtime built without the java.instrument module can't load agents and will refuse to start under the sandbox — unset JAVA_TOOL_OPTIONS in the command for such a tool. The jar ships in the npm package as vendor/java-proxy-agent/srt-proxy-agent.jar (source: vendor/java-proxy-agent-src/; built by the release workflow, or locally with npm run build:java-agent — needs a JDK ≥ 17). If it isn't found, JAVA_TOOL_OPTIONS is left alone and JVMs behave as before; bundlers can point at their own copy with javaAgentJarPath.

Filesystem isolation

Filesystem rules are enforced at the OS level:

  • macOSsandbox-exec with dynamically generated Seatbelt profiles that name the allowed read/write paths
  • Linuxbubblewrap with bind mounts, marking directories read-only or read-write based on your config
  • Windows — additive (OI)(CI) explicit ACEs for the puku-sandbox SID on the configured paths (ALLOW on allowRead/allowWrite, DENY on denyRead/denyWrite), then removed at reset()

Default filesystem permissions:

  • Read (deny-then-allow): allowed everywhere by default. Deny broad regions, then re-allow specific paths inside them. allowRead wins over denyRead.
    • Example: denyRead: ["~/.ssh"] to block access to SSH keys
    • Example: denyRead: ["/Users"], allowRead: ["."] to block all of /Users except the workspace
    • Empty denyRead: [] = full read access (nothing denied)
  • Write (allow-only): denied everywhere by default. You must explicitly allow paths.
    • Example: allowWrite: [".", "/tmp"] to allow writes to current directory and /tmp
    • Empty allowWrite: [] = no write access (nothing allowed)
    • denyWrite creates exceptions inside allowed paths (deny wins)

Precedence is intentionally opposite for reads vs writes: allowRead overrides denyRead, while denyWrite overrides allowWrite. This lets you carve out readable regions inside denied areas, and protected regions inside writable areas.

Mandatory deny paths

A handful of sensitive files and directories are always blocked from writes, even if they fall inside an allowed write path. This is defense-in-depth against sandbox escapes and config tampering.

Always-blocked files:

  • Shell configs: .bashrc, .bash_profile, .zshrc, .zprofile, .profile
  • Git configs: .gitconfig, .gitmodules
  • Other sensitive files: .ripgreprc, .mcp.json

Always-blocked directories:

  • IDE dirs: .vscode/, .idea/
  • Puku config dirs: .puku-cli/commands/, .puku-cli/agents/
  • Git hooks and config: .git/hooks/, .git/config

These are blocked automatically — you don't need to add them to denyWrite. Even with allowWrite: ["."], writing to .bashrc or .git/hooks/pre-commit will fail:

$ srt 'echo "malicious" >> .bashrc'
/bin/bash: .bashrc: Operation not permitted

$ srt 'echo "bad" > .git/hooks/pre-commit'
/bin/bash: .git/hooks/pre-commit: Operation not permitted

Linux note: mandatory deny paths only block files that already exist on Linux. Non-existent files in these patterns can't be blocked by bubblewrap's bind-mount approach. macOS uses globs, which block both existing and new files.

Linux search depth: on Linux, PSRT uses ripgrep to scan for dangerous files inside subdirectories of allowed write paths. By default it searches up to 3 levels deep. Configure with mandatoryDenySearchDepth:

{
  "mandatoryDenySearchDepth": 5,
  "filesystem": {
    "allowWrite": ["."]
  }
}
  • Default: 3 (up to 3 levels deep)
  • Range: 110
  • Higher = more protection, slower startup
  • Files in the working directory (depth 0) are always protected

Unix socket restrictions (Linux)

On Linux, PSRT uses seccomp BPF (Berkeley Packet Filter) to block Unix domain socket creation at the syscall level. This stops the wrapped process from creating new Unix sockets for local IPC (unless explicitly allowed).

How it works:

  1. Baked-in BPF filter — the package ships a static apply-seccomp binary for x64 and arm64 with the seccomp BPF filter compiled in. The filter is architecture-specific but libc-independent, so it works with both glibc and musl.
  2. Runtime detection — PSRT detects your architecture and uses the matching apply-seccomp binary.
  3. Syscall filtering — the BPF filter intercepts the socket() syscall and blocks creation of AF_UNIX sockets by returning EPERM. This stops sandboxed code from creating new Unix sockets.
  4. Two-stage application:
    • Outer bwrap creates the sandbox with filesystem, network, and PID namespace restrictions
    • Network bridging processes (socat) start inside the sandbox (they need Unix sockets)
    • apply-seccomp creates a nested user+PID+mount namespace and remounts /proc
    • Inside the nested namespace, apply-seccomp acts as PID 1 (non-dumpable init/reaper)
    • apply-seccomp forks, applies the seccomp filter via prctl(), and execs the user command
    • The user command runs with all sandbox restrictions plus Unix socket creation blocking

PID namespace isolation: the nested PID namespace ensures the user command can't see or address any process that runs without the seccomp filter (bwrap's init, the shell wrapper, the socat helpers). This keeps the seccomp boundary intact regardless of kernel.yama.ptrace_scope, since unfiltered helpers aren't reachable via ptrace or /proc/N/mem. The inner PID 1 sets PR_SET_DUMPABLE=0 so it's not ptraceable either. If nested namespace creation fails, apply-seccomp aborts rather than running without isolation.

Limitations: the filter blocks socket(AF_UNIX, …) and io_uring_setup/io_uring_enter/io_uring_register (the latter three because IORING_OP_SOCKET on Linux 5.19+ would otherwise bypass the socket() rule). It doesn't prevent operations on Unix socket file descriptors inherited from parent processes or passed via SCM_RIGHTS. For most sandboxing scenarios, blocking socket creation is enough.

Zero runtime dependencies: pre-built static apply-seccomp binaries and pre-generated BPF filters are bundled for x64 and arm64. No compilation tools or external dependencies required at runtime.

Architecture support: x64 and arm64 are fully supported with pre-built binaries. Other architectures aren't currently supported. To run sandboxing without Unix socket blocking on an unsupported architecture, set allowAllUnixSockets: true.

Violation detection and monitoring

When a wrapped process tries to access a restricted resource:

  1. The operation is blocked at the OS level (returns EPERM)
  2. The violation is logged (platform-specific mechanisms)
  3. The user is notified (in Puku CLI, this triggers a permission prompt)

macOS: PSRT taps into macOS's system sandbox violation log store. This gives real-time notifications with details about what was attempted and why it was blocked. This is the same mechanism Puku CLI uses for violation detection.

# Watch sandbox violations in real time
log stream --predicate 'process == "sandbox-exec"' --style syslog

Linux: bubblewrap doesn't have built-in violation reporting. Use strace to trace syscalls and find blocked operations:

# All denied operations
strace -f srt <your-command> 2>&1 | grep EPERM

# Specific file operations
strace -f -e trace=open,openat,stat,access srt <your-command> 2>&1 | grep EPERM

# Network operations
strace -f -e trace=network srt <your-command> 2>&1 | grep EPERM

Advanced: bring your own proxy

For more sophisticated network filtering, you can point PSRT at your own proxy instead of the built-in ones. This enables:

  • Traffic inspection with tools like mitmproxy
  • Custom filtering logic beyond simple domain allowlists
  • Audit logging of every network request

Example with mitmproxy:

# Start mitmproxy with a custom filtering script
mitmproxy -s custom_filter.py --listen-port 8888

Note: custom proxy configuration isn't supported in the new config format yet. This will be added in a future release.

Important security note: even with domain allowlists, exfiltration paths may exist. For example, allowing github.com lets a process push to any repository. With a custom MITM proxy and proper certificate setup, you can inspect and filter specific API calls to prevent this.

Security limitations

  • Network filtering scope: the network filter restricts which domains processes can connect to. It doesn't otherwise inspect traffic passing through the proxy, and you're responsible for only allowing trusted domains in your policy.

  • Privilege escalation via Unix sockets: allowUnixSockets can inadvertently grant access to powerful system services that lead to sandbox bypasses. For example, allowing /var/run/docker.sock effectively grants access to the host system through the docker socket. Carefully consider any Unix socket you allow.

  • Filesystem permission escalation: broad filesystem write permissions can enable privilege escalation. Allowing writes to directories containing executables in $PATH, system configuration directories, or user shell configs (.bashrc, .zshrc) can lead to code execution in different security contexts when other users or system processes access those files.

  • Linux sandbox strength: the Linux implementation provides strong filesystem and network isolation but includes an enableWeakerNestedSandbox mode that lets it run inside Docker environments without privileged namespaces. This considerably weakens security and should only be used when additional isolation is enforced elsewhere.

  • Weaker network isolation (macOS): enableWeakerNetworkIsolation re-enables access to com.apple.trustd.agent, which Go programs need to verify TLS certificates via the macOS Security framework. This opens a potential exfiltration path through the trustd service and should only be enabled when Go TLS verification is required (e.g. using httpProxyPort with a MITM proxy and custom CA).

  • Apple Events (macOS): allowAppleEvents re-enables sending Apple Events and Launch Services open requests ((allow appleevent-send), (allow lsopen), and mach-lookups for com.apple.coreservices.appleevents, com.apple.CoreServices.coreservicesd, and com.apple.coreservices.quarantine-resolver), which open, osascript, and URL-opening helpers need. With these allowed, a sandboxed command can launch arbitrary applications with no user prompt, and launched applications run outside the sandbox entirely — so this option removes code-execution isolation, not just weakens it. Scripting already-running apps via Apple Events is additionally gated by macOS TCC automation consent, but launching via open is not. Only enable this when commands inside the sandbox genuinely need to open URLs or applications.

Known limitations and future work

Linux proxy bypass: PSRT currently uses environment variables (HTTP_PROXY, HTTPS_PROXY, ALL_PROXY) to direct traffic through the proxies. This works for most applications but may be ignored by programs that don't respect these variables, leaving them unable to connect to the internet.

Future improvements:

  • Proxychains support — add proxychains support via LD_PRELOAD on Linux to intercept network calls at a lower level, making bypass harder
  • Linux violation monitoring — add automatic strace-based violation detection for Linux, integrated with the violation store. Currently Linux users must run strace manually to see violations, unlike macOS which has automatic monitoring via the system log store