Rundown includes a Deno-inspired security policy layer that provides explicit allowlist-based permission control for runbook execution.
Rundown provides two layers of security:
The policy engine checks which commands are allowed to run. This layer:
- Blocks denied executables (rm, sudo, curl, etc.)
- Allows approved executables (git, npm, tsc, etc.)
- Can prompt for unknown commands
- Parses complex shell commands including pipes, subshells, and backticks
When sandboxing is enabled (default on supported platforms), Rundown uses OS-level mechanisms to enforce file access:
| Platform | Mechanism | Requirement |
|---|---|---|
| Linux | Landlock LSM | Kernel 5.13+ with Landlock enabled |
| macOS | Seatbelt (sandbox-exec) | macOS 10.5+ |
| Windows | Not supported | Use WSL |
The sandbox enforces:
- Read-only access to allowed paths
- Read-write access to specific directories
- Blocking of denied paths at the kernel level
# Enable sandboxing (default on supported platforms)
rundown run <file> --sandbox
# Disable sandboxing (trust mode)
rundown run <file> --no-sandbox
# Fail if sandbox unavailable (strict mode)
rundown run <file> --sandbox-strictWithout sandbox (--no-sandbox or unsupported platform):
- File read/write policies are NOT enforced
- Commands can access any file the user can access
- Use only with trusted runbooks
Interpreter bypass: Allowing interpreters (python, node, sh) grants unrestricted code execution. The sandbox limits file access, but the interpreter can execute arbitrary logic.
The security policy layer enforces a default-deny model:
- Commands not in the allowlist require user confirmation
- Deny lists take precedence over allow lists
- Granular control over commands, file access, and environment variables
- Session grants provide temporary permissions without modifying config
- CLI flags can override policy for specific runs
The policy layer protects against:
| Threat | Protection |
|---|---|
| Arbitrary command execution | Allowlist controls which commands can run |
| Sensitive file access | Path patterns + OS sandbox restrict read/write operations |
| Credential leakage | Environment variable filtering blocks secrets |
| Runbook tampering | Per-runbook overrides allow different trust levels |
| Command injection via backticks | Parser extracts and checks commands in backticks |
┌─────────────────────────────────────────────────────┐
│ User │
│ - Approves permission prompts │
│ - Configures policy files │
│ - Uses CLI flags for temporary overrides │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Policy Evaluator │
│ - Checks commands against allow/deny lists │
│ - Applies runbook-specific overrides │
│ - Manages session grants │
│ - Extracts commands from backticks and $() │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ OS-Level Sandbox │
│ (Linux: Landlock | macOS: Seatbelt) │
│ - Enforces file read/write restrictions │
│ - Kernel-level enforcement (cannot be bypassed) │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Runbook │
│ - Defines steps and commands │
│ - Subject to policy restrictions │
└─────────────────────────────────────────────────────┘
File-backed data sources (--var items=file:data.txt) are subject to security controls:
- Symlink resolution:
fs.realpath()resolves symlinks before path validation - Path containment: Resolved paths must stay within the project root directory
- Blocked sources: Paths escaping the project are silently ignored with a warning
- Drift detection: File snapshots (size, mtime, SHA-256 of first 64 KiB) detect modification between iterations
- Iteration cap:
MAX_FILE_ITERATIONS(10,000) prevents runaway loops from unbounded file sources
These controls are always active, independent of the sandbox layer.
Policy configuration is discovered in the following order (highest to lowest priority):
.rundownrc(JSON or YAML).rundownrc.json.rundownrc.yaml/.rundownrc.ymlrundown.config.js/.cjs/.mjspackage.json(rundownfield)
# .rundownrc.yaml
version: 1
default:
# Policy mode: 'prompted' | 'execute' | 'deny'
mode: prompted
# Command execution rules
run:
allow:
- git
- npm
- node
deny:
- sudo
- rm
# File read rules (supports {repo}, {tmp} placeholders)
read:
allow:
- "{repo}/**"
- "{tmp}/**"
deny:
- "**/.env"
- "**/credentials.json"
# File write rules
write:
allow:
- "{repo}/.claude/**"
- "{repo}/dist/**"
deny:
- "**/.env"
# Environment variable rules (supports glob patterns)
env:
allow:
- PATH
- HOME
- NODE_ENV
- npm_*
deny:
- "*_TOKEN"
- "*_SECRET"
- AWS_*
# Runbook-specific overrides
overrides:
- runbook: "deploy/*.runbook.md"
mode: execute # Trusted deployment scripts
run:
allow:
- docker
- kubectl
# Persisted grants
grants:
- type: run
pattern: curl
scope: permanentWhen no configuration file is found, Rundown uses built-in defaults:
Allowed commands:
- Version control:
git - Node.js ecosystem:
node,npm,npx,pnpm,yarn,bun - Build tools:
tsc,esbuild,vite,webpack,rollup - Linting:
eslint,prettier,biome - Testing:
jest,vitest,mocha,playwright,cypress - Other languages:
python,python3,pip,pip3,go,cargo,rustc,make,cmake - Rundown:
rd,rundown
Denied commands:
- System administration:
sudo,su,passwd,useradd,usermod,userdel,chown,chmod - Network tools:
curl,wget,nc,netcat,ncat,ssh,scp,sftp,rsync - Destructive operations:
rm,rmdir,mv,dd,mkfs,fdisk,parted - Process control:
kill,killall,pkill - Container tools:
docker,podman,kubectl,helm
Default file access:
- Read allow:
{repo}/**,{tmp}/** - Read deny:
**/.env,**/.env.*,**/credentials.json,**/*secret*,**/*password*,**/id_rsa,**/id_ed25519,**/*.pem,**/*.key - Write allow:
{repo}/.claude/**,{repo}/node_modules/**,{repo}/dist/**,{repo}/build/**,{repo}/.next/**,{tmp}/** - Write deny:
**/.env,**/.env.*,**/credentials.json,**/*secret*,**/*password*
Note: Read deny includes SSH keys and certificates (id_rsa, id_ed25519, *.pem, *.key) to prevent credential exfiltration, but write deny does not include these patterns to allow key generation workflows.
Allowed environment variables:
- System:
PATH,HOME,USER,SHELL,TERM,LANG,LC_*,TMPDIR,TMP,TEMP - Development:
CI,NODE_ENV,DEBUG,npm_*,RUNDOWN_*
Denied environment variables:
- Tokens/secrets:
*_TOKEN,*_KEY,*_SECRET,*_PASSWORD,*_CREDENTIAL - Cloud credentials:
AWS_*,GCP_*,AZURE_*,GOOGLE_* - Infrastructure:
KUBECONFIG,DOCKER_*,SSH_*,GPG_* - Specific tokens:
GITHUB_TOKEN,GITLAB_TOKEN,NPM_TOKEN
| Mode | Behavior |
|---|---|
prompted (default) |
Ask user for permission on unlisted commands |
execute |
Allow all commands without prompting |
deny |
Block all unlisted commands without prompting |
# Use prompted mode (default)
rundown run deploy.runbook.md
# Trust mode - skip all policy checks
rundown run deploy.runbook.md --allow-all
# Strict mode - block everything not explicitly allowed
rundown run deploy.runbook.md --deny-allPattern matching uses picomatch for glob syntax. Command parsing uses shell-quote to extract executables from complex shell commands.
Commands are matched by their executable name:
run:
allow:
- git # Matches: git status, git push, etc.
- npm # Matches: npm install, npm test
deny:
- sudo # Blocks: sudo anythingFor piped commands, shell wrappers, and logical operators, all executables are extracted and checked:
git log | grep fix # Both 'git' and 'grep' must be allowed
sh -c "npm install" # 'sh' and 'npm' must be allowed
npm test && npm build # 'npm' must be allowedPath patterns support glob syntax with special placeholders:
| Placeholder | Resolves To |
|---|---|
{repo} |
Repository root (defaults to process.cwd()) |
{tmp} |
System temporary directory (e.g., /tmp on Unix, %TEMP% on Windows) |
read:
allow:
- "{repo}/**" # All files in repo
- "{tmp}/rundown-*" # Temp files with prefix
deny:
- "**/.env" # All .env files
- "**/secrets/**" # Any secrets directoryVariable names support glob patterns:
env:
allow:
- PATH
- NODE_ENV
- npm_* # All npm_ prefixed vars
- RUNDOWN_* # All Rundown vars
deny:
- "*_TOKEN" # Any token
- "*_SECRET" # Any secret
- AWS_* # All AWS credentials| Option | Description |
|---|---|
--allow-run <cmds> |
Allow specific commands (comma-separated) |
--allow-read <paths> |
Allow reading specific paths (comma-separated) |
--allow-write <paths> |
Allow writing to specific paths (comma-separated) |
--allow-env <vars> |
Allow specific environment variables (comma-separated) |
--allow-all |
Bypass all policy checks (trust mode) |
--deny-all |
Block all operations not explicitly allowed |
--policy <file> |
Use a specific policy configuration file |
-y, --yes |
Skip confirmation prompts (auto-approve) |
--non-interactive |
Non-interactive mode (auto-deny, CI-friendly) |
--sandbox |
Enable OS-level sandbox (default on supported platforms) |
--no-sandbox |
Disable sandbox enforcement |
--sandbox-strict |
Fail if sandbox is unavailable |
Note: If both --allow-all and --deny-all are specified, --deny-all takes precedence.
Note: --allow-all implies --no-sandbox (trust mode bypasses all enforcement).
Permissions are evaluated in this order (first match wins):
- CLI grants (
--allow-run, etc.) - highest priority - Session grants (user-approved during prompts)
- Deny list - if matched, operation is blocked
- Allow list - if matched, operation is allowed
- Policy mode -
prompted,execute, ordeny
# Allow specific commands for this run
rundown run deploy.runbook.md --allow-run docker,kubectl
# Allow file operations
rundown run backup.runbook.md --allow-read /var/log --allow-write /backup
# CI/CD: auto-approve with pre-approved commands
rundown run test.runbook.md --yes --allow-run npm,jest
# CI/CD: strict mode with no prompts
rundown run test.runbook.md --non-interactive
# Use custom policy file
rundown run deploy.runbook.md --policy ./ci-policy.yamlDifferent runbooks can have different trust levels:
overrides:
# Trust deployment scripts
- runbook: "deploy/**/*.runbook.md"
mode: execute
run:
allow:
- docker
- kubectl
- helm
# Strict mode for untrusted runbooks
- runbook: "community/**/*.runbook.md"
mode: deny
# Additional permissions for specific runbook
- runbook: "backup.runbook.md"
run:
allow:
- rsync
- tarOverride patterns use glob matching against runbook file paths.
When prompted for permission, users can choose grant scope:
| Option | Effect |
|---|---|
| Allow once | Single operation only |
| Allow for this session | Remembered until runbook completes |
| Allow all [type] for this session | Allow all operations of that type |
| Deny once | Single denial |
| Deny all [type] for this session | Block all operations of that type |
Session grants are memory-only and do not persist to disk. They are cleared when:
- The runbook completes
- The CLI process exits
- The user explicitly resets them
- Start with the default policy and add specific permissions as needed
- Use
--allow-runfor one-off operations rather than modifying config - Prefer
promptedmode overexecutefor production runbooks
Create .rundownrc.yaml in your repository with project-specific allowlists:
version: 1
default:
mode: prompted
run:
allow:
- turbo # Project uses Turborepo
- prisma # Database migrationsFor CI environments:
# Option 1: Pre-approve known commands
rundown run test.runbook.md --yes --allow-run npm,jest,eslint
# Option 2: Use non-interactive mode with policy file
rundown run test.runbook.md --non-interactive --policy ./ci-policy.yaml
# Option 3: Trust mode for controlled CI environment
rundown run deploy.runbook.md --allow-allCreate a dedicated CI policy file:
# ci-policy.yaml
version: 1
default:
mode: execute # No prompts in CI
run:
allow:
- npm
- node
- git
- jest
- eslint
deny:
- sudo
- curl
- wgetReview your policy configuration periodically:
- Check for overly permissive
allowpatterns - Ensure
denylists include sensitive operations - Review runbook overrides for appropriate trust levels
- Monitor session grants during development
# Check if Landlock is enabled in kernel
cat /sys/kernel/security/lsm
# Output should include 'landlock'
# Check kernel version (requires 5.13+)
uname -rIf Landlock is not available:
- Upgrade to Linux kernel 5.13 or later
- Ensure CONFIG_SECURITY_LANDLOCK is enabled in kernel config
- Install a Landlock wrapper like landrun (v0.1.0 or later recommended)
The sandbox-exec command is built into macOS but may require adjustments:
# Verify sandbox-exec exists
which sandbox-exec
# Should output: /usr/bin/sandbox-execCommon issues:
- SIP restrictions: System Integrity Protection may block certain sandbox operations
- Entitlements: Some apps may need specific entitlements for sandbox access
When sandbox is unavailable:
| Scenario | Behavior |
|---|---|
--sandbox (default) |
Falls back to unsandboxed with warning |
--sandbox-strict |
Fails with error, command not executed |
--no-sandbox |
Executes without sandbox, no warning |
To debug sandbox-related issues:
# Run with sandbox strict to see if sandbox is available
rundown run test.runbook.md --sandbox-strict
# If it fails, check availability:
# Linux: cat /sys/kernel/security/lsm
# macOS: which sandbox-exec
# Disable sandbox for trusted runbooks
rundown run trusted.runbook.md --no-sandbox