ALLOW AGENCY. DENY DISASTER.
Saya is the scabbard you wrap around your LLM workflows.
Built for macOS (leveraging native Seatbelt sandboxing) and Linux (Landlock/Namespaces), it sits between the agent and its tools as an explicit boundary of intent. Whether your agent is writing files, installing dependencies, or streaming out code, Saya enforces scabbard policies at the kernel edge: direct network stays denied unless you declare a route, workspace access stays sealed unless you grant it, and dangerous output can be masked or the process can be instantly SIGKILLed before damage escapes the enclosure.
If you want a secure, transparent, low-overhead boundary for autonomous execution, this is it.
Philosophy Β· Installation Β· Usage Β· Default Policy Β· Config Β· Output Streams Β· Performance
"Saya (Scabbard) is for keeping the sword from being drawn."
A scabbard does not make the blade weaker; it makes the blade safe to wield with purpose. Saya does not restrict agents because they are useless without trust. It exists because a powerful tool without a boundary will eventually cut the wrong thing: your workspace, your secrets, or your supply chain.
saya ( /sΙΛjΙΛ/ β’ SΓ€h-yΓ€ ) stands between the agent and the execution, serving as the definitive boundary. We do not rely on heavy container abstractions. Trust is built directly into the OS kernel via macOS Seatbelt or Linux Landlock. The agent is the "Brain" and the tools are the "Hand". An autonomous LLM must be allowed to think freely, but the tools it wieldsβwhether self-authored scripts or blindly downloaded third-party "Skills"βare like a drawn and unsheathed sword. As real-world incidents have shown, executing black-box tools without control is inherently dangerous. Without a physical boundary, an agent might:
- Hallucinate a catastrophic
rm -rf /command during a routine cleanup task. - Be tricked by a prompt injection into stealing your SSH keys/AWS credentials and exfiltrating them via
curlto a malicious server. - Execute a seemingly helpful Python or Node.js "Skill" that secretly contains malicious post-install scripts.
saya guarantees that every tool execution happens strictly within a secure, silent, and transparent scabbardβlocking away these dangers while preserving the speed of the strike.
The scabbard's purpose is not merely to contain the blade, but to ensure the wielder has consciously decided to draw it. When you grant --allow-read ${CWD} or --allow-exec /usr/bin/python3, you are not trusting the environment by accident; you are declaring Intent. This is the line between deliberate agency and an uncontrolled strike.
The scabbard philosophy becomes concrete through four operating promises:
| Philosophy | Mechanism | Result |
|---|---|---|
| Intentional access | Deny-by-default policy merging | Nothing opens unless you say so |
| Boundary integrity | Seatbelt / Landlock / Namespaces | The enclosure is enforced by the OS, not by convention |
| Real-time safety | Judgement Pipe | Secrets can be masked or the process can be stopped before output lands |
| Proof of execution | Audit Logger | Every meaningful event can be recorded as evidence |
saya operates as a parent supervisor process that intercepts I/O and enforces policy on the command using native kernel APIs.
graph TD
User[User / CLI] -->|runs| Governor[Governor]
Governor -->|enforces| Sandbox[Sandbox: Seatbelt / Landlock]
Governor -->|spawns in proxy mode| Proxy[Gatekeeper: saya-proxy]
Sandbox -->|wraps| Command[Command]
Proxy -->|announces| Ready[SAYA_PROXY_READY:127.0.0.1:PORT]
Ready --> Governor
Governor -->|injects HTTP_PROXY / HTTPS_PROXY / ALL_PROXY| Command
Command -->|CONNECT tunnel| Proxy
Proxy -->|allowed domains only| Internet[Remote Endpoint]
Command -->|stdout/stderr| Pipes[Anonymous Pipes]
Pipes -->|streams to| JudgementPipe[Judgement Pipe]
JudgementPipe -->|Verdict: Pass| Output[Stdout/Stderr]
JudgementPipe -->|Verdict: Kill| Signal[SIGKILL]
JudgementPipe -->|Verdict: Audit| EventChannel[Event Channel]
EventChannel -->|submits to| Governor
Governor -->|writes to| AuditLogger[Audit Logger]
Signal --> Command
Each component name is deliberate: Saya is not a pile of subsystems, but a coordinated enclosure with one jobβletting agency happen without letting intent dissolve.
- Governor: The
sayaprocess itself. It handles policy loading, event loops, and command lifecycle. - Seatbelt / Landlock: Uses macOS
Seatbeltor LinuxLandlockto forge the kernel boundary at the syscall level. This is notptrace; it cannot be bypassed by the command. - Gatekeeper: The
saya-proxycompanion local-only CONNECT proxy thatsayalaunches only for--allow-proxymode. It binds to127.0.0.1, applies merged domain allow/deny rules, and lets Saya keep direct outbound sockets blocked for the sandboxed command. - Judgement Pipe: A SIMD-accelerated "Chain of Responsibility" that inspects every byte of
stdoutandstderrto scan for and mask or kill sensitive patterns in real-time. - Audit Logger: An immutable JSONL sink for forensic events. If the audit log cannot be written, the command is immediately terminated. "If we can't record it, it didn't happen."
Powered by macOS Seatbelt or Linux Landlock.
- Deny Default: The command starts with NO access to the user's file system.
- Explicit Allow: You must whitelist every directory or file in the user workspace.
- Network Block: Network sockets are denied by default.
curlfails instantly. - TOCTOU Defense: Saya provides recursive symlink resolution to prevent Time-of-Check to Time-of-Use attacks, ensuring that malicious link-swapping during execution is detected.
- Env Deny Default:
sayascrubs all sensitive environment variables (e.g.,AWS_ACCESS_KEY_ID) from the command. - Explicit Allow/Deny: Use
--allow-envto add specific variables, or--deny-envto strip them.
saya watches the output stream.
- Dual-Engine Pipeline: Define exact string (
match) or regular expression (regex_match) rules insaya.yaml. - Action: Kill: If a rule matches (e.g.,
sk-[a-zA-Z0-9]{32}), the process isSIGKILLed before the output reaches the user/agent. - Action: Mask: Redact sensitive data from the stream itself.
Logs are written in JSONL format.
- Session Traceability: Every entry is tagged with a
uuidsession ID. - Rich Context:
session_startrecords the exact command, CWD, and user. - Strict Exit Codes: Differentiate between "Process Failed" and "Policy Kill".
# Clone and build from source
git clone https://github.com/saya-run/saya.git
cd saya
# Install the binaries (saya + saya-proxy) and deploy the default System-Global config
make installmake install keeps saya and saya-proxy together in ${PREFIX}/bin. Proxy mode fails shut if the helper cannot be found, so install both binaries together or point SAYA_PROXY_BIN at the helper explicitly.
To ensure saya correctly parses its own flags versus the arguments of your target tool, you must use the -- separator:
saya run [SAYA_OPTIONS] -- <COMMAND> [CMD_ARGS]Important
Correct: saya run -- python3 agent.py
Incorrect: saya run python3 agent.py β Saya will misinterpret python3 as one of its own options.
If the target command itself has flags (for example --help), keep them after the separator:
saya run -- /bin/echo --help.
For command reference, use:
saya --help
saya run --help
saya inspect --help--allow-proxy is implemented by the Gatekeeper companion binary saya-proxy.
sayalaunches it only for proxy mode, waits forSAYA_PROXY_READY:127.0.0.1:PORT, then injectsHTTP_PROXY,HTTPS_PROXY, andALL_PROXYinto the command while clearingNO_PROXY.- The Gatekeeper binds only to
127.0.0.1on an ephemeral port and enforces the mergedallow_proxy/deny_proxypolicy. Deny entries win, and an empty allow-list means "allow all except denied". - When a CONNECT target is denied, Saya records the blocked
FQDN:PORTin the diagnostic log (if--logis enabled) and in the audit JSONL (if--audit-logis enabled), so you can tune policy or investigate suspicious binaries. - The helper currently tunnels HTTP
CONNECT, so HTTPS clients work naturally; some plain HTTP workflows may need an explicit tunnel mode such ascurl --proxytunnel. - If the helper cannot be located, started, or handshaked,
sayafails shut before your command runs.
Saya is Deny-by-Default at its core. To make that usable on day one without softening the boundary, the default System-Global config (/etc/saya/config.yaml) deployed via make install provides a baseline for ordinary OS operation while keeping your private workspace sealed.
| Category | Default Permission | Rationale |
|---|---|---|
| System Paths | Read-Only | Allows standard tools (ls, python3, node) and dynamic linkers to function. |
| Workspace | Denied | Prevents accidental leakage of .env, .git, or working drafts "just because they existed." |
| Network | Denied | Absolute block on exfiltration. curl and wget fail instantly. |
| Environment | Filtered | Only PATH, USER, HOME, TERM, LANG are passed. Secrets are scrubbed. |
| Child Processes | Denied | Spawning of sub-processes is blocked. You must explicitly allow tools for the Command to use them. |
Example: After install, saya run -- ls will work (executing a system tool), but saya run -- cat ./README.md will fail.
This failure is not a bug; it is the Security of Intent in action. saya does not "read the air" or assume your current directory is safe just because you are in it. The scabbard stays closed until you make a conscious choice to open part of it.
By default, the enclosure blocks network, blocks reading and writing unless you grant them, and blocks child process spawning except for explicitly allowed executables.
# Fails because 'curl' tries to access the network
saya run -- curl -s https://example.com
# Fails because 'sh' tries to spawn '/bin/ls' as a child process (Default Deny)
saya run -- /bin/sh -c "/bin/ls"When an agent must install JavaScript dependencies, proxy mode lets you narrow egress to only the package registry. If you pass an explicit allow-list to --allow-proxy, every other domain is denied by default.
# Example: only allow npm registry access
saya run \
--allow-read "${CWD}" \
--allow-write "${CWD}" \
--allow-proxy registry.npmjs.org \
-- npm install --ignore-scriptsThis does not make arbitrary packages trustworthy. It does prevent the sandboxed npm process from reaching non-approved domains during dependency resolution, which is useful for containing supply-chain pivots and unexpected post-install fetches.
Add --deny-proxy only when you are using open proxy mode (--allow-proxy with no domains) or when you want explicit deny entries to override a broader allow-list.
To ensure accountability, saya can record every action, policy kill, and runtime violation into a JSONL file.
# If any audit write fails (session start/runtime/session end), Saya fails shut (exit 11).
# "If we can't record it, it didn't happen."
saya run --audit-log ./evidence.jsonl -- python3 agent_task.pyIdeal for LLM Agent workflows. By default, saya keeps operational logs silent unless you explicitly route them with --log. Using --stealth forces those operational logs to stay suppressed, ensuring the agent only sees the pure output of the script it executed. Policy violations are still recorded if you enable the Audit Log.
saya run --stealth --audit-log ./evidence.jsonl -- node generate_code.jsTo verify the final merged policy before execution, use the inspect command. This displays the integrated policy in YAML format without running the target command.
# Verify policy merging from multiple sources
saya inspect --manifest project.yaml --allow-read /tmp -- python3 script.pysaya enforces scabbard policies through a strict hierarchy and unified schema.
- System-Global (
/etc/saya/config.yaml) - The Absolute Law. Defined by the administrator. - Project-Local (
./saya.yaml) - Specific rules for the current task. - Explicit Overrides (
--manifestor flags) - Instant overrides for the current execution.
saya core does not "read the air." It will not assume that your current directory is safe. It demands Intent. To achieve zero-friction agent programming with the current implementation, define a baseline acceptance in your project-local manifest (./saya.yaml).
# Example: Transparent local development
allow_read:
- ${CWD} # Explicitly allow reading the current workspacesaya core does not "read the air." It will not assume that your current directory is safe just because you are in it. It demands Intent.
- Explicit over Implicit: You codify the will that "This project is open for agency."
- No Environmental Leaks: By blocking
${CWD}by default, your home directory, staging files, and private workspace are safe from accidental agent curiosity. - System Integrity: Your project is open, but the rest of the OS remains sealed. This is the Principle of Least Privilege.
Silent by Design: Intent is only half of the discipline. Saya also refuses to flood the agent with operational noise. The scabbard speaks when the blade tries to escape, not while it is being carried safely.
Both global (/etc/saya/config.yaml) and local (./saya.yaml) manifests follow the same top-level structure:
# Permissions (Additive)
allow_read: [/paths]
allow_write: [/paths]
allow_exec: [/binaries]
allow_env: [VAR_NAMES]
# Deny-lists (Absolute Priority)
deny_read: [/paths]
deny_write: [/paths]
deny_exec: [/binaries]
deny_env: [VAR_NAMES]
# Proxy & Pipeline
allow_proxy: [domains]
deny_proxy: [domains]
allow_net: true | false # Optional compatibility/global-boundary switch
pipelines:
- name: "Identifier"
match: "Exact String"
regex_match: "Regex"
action: kill | mask | notify
# Global Configuration Only
audit_log: /path/to/audit.jsonl| Field | Type | Description |
|---|---|---|
allow_read |
List | Paths granted read access. Supports ${CWD}, ~, and env vars. |
deny_read |
List | Paths explicitly forbidden. Absolute priority. |
allow_write |
List | Paths granted write access. |
deny_write |
List | Paths explicitly forbidden to write. |
allow_proxy |
List | Proxy allow-list domains. Empty list means allow all (deny list still applied). |
deny_proxy |
List | Proxy deny-list domains. Evaluated first and overrides allow entries. |
allow_net |
Bool | Compatibility/global-boundary switch for direct network allow; prefer CLI network mode flags for runtime control. |
allow_exec |
List | Binaries allowed for child process spawning (e.g. tools run by an agent). |
deny_exec |
List | Binaries explicitly forbidden from being spawned as child processes. |
allow_env |
List | Environment variables to explicitly pass (Overrides Env Basics). |
deny_env |
List | Environment variables to explicitly strip (Overrides Env Basics). |
pipelines |
List | A list of data stream interception rules (Exact Match or Regex). |
Tip
The Trusted Entry Point: The command you explicitly type (e.g., saya run -- python3) is always implicitly trusted and authorized. If the OS allows you to execute it, Saya allows it to start the enclosure. allow_exec is used to regulate the child processes that the initial entry point might try to spawn.
βΉοΈ System Basics (Implicit Read-Only)
sayarelies on the system-global configuration file (/etc/saya/config.yaml) to grant Read-Only access to the underlying OS paths (/usr/lib,/usr/bin,/System, etc.) required for dynamic linking (dyld) and basic OS operation. Without this config, the sandbox defaults to absolute zero trust and will instantly terminate most processes.
βΉοΈ Env Basics (Implicit Pass-Through) Similarly,
sayauses the global config to pass through a minimal set of standard variables (e.g.,PATH,USER,HOME). If the global config is missing or these are removed, no environment variables are passed to the command.
When configuring pipelines in a manifest, use the following syntax:
| Field | Description |
|---|---|
name |
Human-readable identifier. |
match |
A pure string pattern for Exact Match operations (extremely fast). |
regex_match |
A Rust-compatible Regular Expression (slower, but flexible). |
action |
kill (terminate), notify (audit log only), or mask. |
Define your interception rules in saya.yaml or /etc/saya/config.yaml:
# Example: Advanced Data Interception Policy
pipelines:
- name: "mask_aws_keys"
regex_match: "AKIA[A-Z0-9]{16}"
action: mask # Redact matching patterns with **********
- name: "stop_secret_leak"
match: "INTERNAL_PRIVATE_KEY"
action: kill # Instantly SIGKILL the process if this string appearsTo achieve the best balance of safety and agency in autonomous agent workflows, we recommend the following professional baseline:
# ./saya.yaml
allow_read:
- "/usr/bin/python3*" # Runtime
- "/usr/local/lib/python3.*/site-packages" # Dependencies
- "${CWD}" # Current Workspace (Recursive)
deny_read:
- "~/.ssh" # Critical Secrets
- "~/.aws"
- "/etc/passwd"
allow_net: false # Force local-only agency
allow_exec:
- "/usr/bin/python3"
- "/usr/bin/node"
pipelines:
- name: "leak_protection"
regex_match: "(sk-|AKIA|ghp_)[A-Za-z0-9_-]{20,}"
action: mask # Mask API Keys (OpenAI, AWS, GitHub)
- name: "safety_kill"
match: "rm -rf /" # Absolute Disaster Recovery
action: killWant to see it in action? Create a script that "accidentally" leaks a key:
# leak.sh
echo "Connecting with key: AKIA1234567890ABCDEF"Run it with a masking rule:
saya run --allow-read . -- /bin/sh leak.sh
# Output: Connecting with key: ********************If you set the action to kill, Saya terminates the process before the secret hits your screen:
saya run --allow-read . -- /bin/sh leak.sh
# Output: [saya] 2026-02-28 18:00:00 [ERROR] Violation: stop_secret_leak matched. SIGKILL sent.
# (Process terminated with exit code 137)You can define policies directly via CLI arguments. These override saya.yaml settings.
| Flag | Description |
|---|---|
--allow-read <PATH> |
Whitelist a file or directory for reading. |
--allow-write <PATH> |
Whitelist a file or directory for writing. |
--allow-net |
Full direct network allow (no proxy, no proxy-domain filtering). |
--allow-proxy [DOMAINS] |
Enable proxy mode. No value = allow all domains except deny-list. Value given = proxy allow-list. |
--deny-proxy <DOMAINS> |
Add proxy deny-list domains (comma-separated or repeated). |
--allow-exec <EXEC> |
Allow executing from these paths (Default: Denied). |
--allow-env <VAR> |
Allow specific environment variables (Default: Env Basics). |
--deny-read <PATH> |
Explicitly deny reading (overrides allow). |
--deny-write <PATH> |
Explicitly deny writing (overrides allow). |
--deny-exec <EXEC> |
Explicitly deny executing specific binaries. |
--deny-env <VAR> |
Explicitly deny specific environment variables (Overrides Env Basics). |
--strict-config |
Treat invalid policy inputs (e.g. missing allow-paths) as hard config errors. |
--log <LOG_PATH> |
Write operational warnings to file (default: disabled/silent unless set). |
--audit-log <AUDIT_PATH> |
Write JSONL forensic events to this file. |
--stealth |
Suppress all operational output from saya itself. |
Proxy / network mode precedence:
--allow-netβ full allow (no proxy).--allow-proxy(no value) β proxy allow-all + deny-list.--allow-proxy dom1,dom2β proxy allow-list + deny-list.- No network flags β deny all network.
When proxy mode is active, saya launches the Gatekeeper (saya-proxy) on 127.0.0.1:0, reads SAYA_PROXY_READY:127.0.0.1:PORT, and passes merged domain policy via SAYA_PROXY_DOMAINS and SAYA_PROXY_DENY_DOMAINS.
To ensure you never "forget" to use the scabbard, you can alias dangerous commands in your shell profile (.zshrc or .bashrc).
By combining aliases with --stealth, saya becomes truly invisible. You get the exact same Terminal UX, but with a kernel-level safety net protecting your underlying OS.
# In your .zshrc
alias npm='saya run --stealth -- /usr/local/bin/npm'
alias pip='saya run --stealth -- /usr/local/bin/pip'
alias curl='saya run --stealth -- /usr/bin/curl'saya manages three distinct streams of information to prevent operational noise from polluting the agent's context window.
- Source:
--audit-log(File) - Format: JSONL (Strictly Typed)
- Purpose: Immutable, machine-readable forensic trail. This is the Single Source of Truth for system events.
- Source: Command Stdout/Stderr
- Format: Raw Text (Unchanged)
- Purpose: The LLM's reasoning and tool output.
- Behavior:
sayapasses this through instantly to the console unless a safety rule triggers a kill.
- Source: The destination selected by
--log(for example/dev/stderr,/dev/stdout, or a file) - Format: Human-readable text (
[saya] YYYY-MM-DD HH:MM:SS LEVEL message) - Purpose: Operational feedback (Startup, Policy details, Security violations, Fatal errors).
- Behavior: Silent by default. Emitted only when
--logis set, and suppressed completely with--stealth. - Stealth & Audit: Even when using
--stealth, specifying--audit-logwill ensure all security violations are recorded to the JSONL file without appearing in the console.
When a command is denied access to a file or network, saya follows a Silent Fail-Shut policy for security. To debug why a policy is blocking you:
- Check the Log: Run with
--log debug.logto see explicit denial reasons.saya run --log debug.log -- <CMD> # grep "denied" debug.log
- Use Strict Config Validation: Add
--strict-configto fail fast on invalid policy paths instead of warning-and-skip behavior. - Inspect the Audit Trail: Use
--audit-log audit.jsonlto see high-level security events.- In proxy mode, denied domains are recorded with the attempted
FQDN:PORT, which makes it easier to tighten allow-lists or spot unexpected egress attempts.
- In proxy mode, denied domains are recorded with the attempted
- Validate Config: Use
make testto ensure your system-global configuration is properly loaded.
saya uses strict exit codes to tell you why it stopped.
| Code | Meaning | Description |
|---|---|---|
0 |
Success | The command exited successfully. |
10 |
Config Error | saya.yaml or global config is invalid. |
11 |
Audit Failure | Critical! Could not write to audit log. Process killed to prevent evidence tampering. |
126 |
Sandbox Failed | Security protection (Seatbelt/Landlock) could not be applied. Fail-Shut triggered. |
127 |
Exec Failed | Could not execute the specified command. |
137 |
Policy Kill | saya terminated the process due to a policy violation. |
* |
Passthrough | Any other code is the command's own exit code. |
Docker and other container runtimes are designed for deployment. saya is designed for Agency at Scale.
Typical containers require a daemon, file system layering, and network stack virtualization, leading to startup times between 500ms and 2,000ms. In contrast, saya leverages native macOS kernel primitives to provide instant protection with negligible overhead.
Measurements taken in the current macOS environment across 400 serial executions of /usr/bin/true. Proxy-mode fetch timings use a local Go static file server serving 4KB and 1MB HTML files over loopback.
| Mode | Mean Latency | Net Overhead |
|---|---|---|
| Native (No Saya) | 1.21ms | - |
| Default (Sandboxed) | 8.12ms | 6.91ms |
| Full-Audit (Forensic) | 8.30ms | 7.09ms |
Proxy Mode (--allow-proxy) |
10.51ms | 9.31ms |
The ~6.9ms default overhead reflects the cost of initializing the macOS Seatbelt profile and applying the kernel-level sandbox. Enabling the forensic audit path adds only ~0.18ms on top of that baseline. Proxy mode adds another ~2.4ms to start the Gatekeeper (saya-proxy) and wire the loopback CONNECT path.
For an LLM agent executing a 1-second Python script, a ~6.9ms startup overhead remains effectively zero. This keeps safety from becoming a bottleneck even in high-frequency tool-use scenarios.
Loopback fetches against a local static server show the additional cost of routing through the Gatekeeper (saya-proxy) compared with direct --allow-net.
| Mode | 4KB Fetch | 1MB Fetch |
|---|---|---|
| Native | 4.94ms | 5.48ms |
| Allow-Net | 12.34ms | 12.99ms |
| Proxy (Allow-List) | 16.18ms | 17.40ms |
| Proxy (Open) | 16.19ms | 17.56ms |
In this benchmark, proxy routing adds roughly ~4ms over direct --allow-net on local loopback fetches. There was no meaningful difference between allow-listed proxy mode and open proxy mode in this implementation.
While initialization is virtually instantaneous, saya must also inspect vast amounts of stdout/stderr output in real-time. Saya utilizes a SIMD-accelerated Dual-Engine architecture to ensure the Judgement Pipe remains frictionless.
Measurements stream a 50.0 MB file through /bin/cat.
| Engine | Mean Time (ms) | Throughput |
|---|---|---|
| Passthrough (No Rules) | 68.1ms | 734.2 MB/s |
| Exact Match (Kill) | 67.2ms | 744.6 MB/s |
| Exact Match (Mask) | 66.9ms | 747.9 MB/s |
| Regex Match (Kill) | 66.0ms | 758.0 MB/s |
| Regex Match (Mask) | 66.8ms | 748.7 MB/s |
Measured throughput was ~734-758 MB/s across passthrough, exact-match, and regex pipelines on a 50MB local stream. In this benchmark, rule evaluation did not materially reduce throughput.
- OS: macOS (Intel or Apple Silicon) or Linux (x86_64 or ARM64)
- Kernel (Linux): v5.13+ (Required for Landlock and unshare support)
- Privileges: Standard User (Root required for Global Config
/etc/saya/config.yaml)
We are actively working on making saya more accessible:
- Homebrew Formula:
brew install saya-run/saya/saya - cargo-binstall: Support for binary-only Rust installation.
- Pre-built Binaries: GitHub Action-powered asset deployment for all architectures.
- Shell Plugins: Native
zsh/fishcompletions and integration guides.
We value integrity and simplicity. Please read CONTRIBUTING.md before submitting a PR.
MIT License. See LICENSE for details.
