Agent security configuration generator — translates canonical security rules into agent-specific configs
Project description
Agent security configuration generator — translates canonical security rules into agent-specific configs.
The Problem
AI coding agents (Claude Code, Copilot CLI, etc.) each have their own permission model and configuration format. Maintaining security rules independently per agent leads to configuration drift, and coverage gaps.
Meanwhile, Anthropic's Sandbox Runtime Tool (SRT) enforces OS-level restrictions (filesystem deny, network allowlists) for Bash commands via kernel sandboxing. But SRT cannot control an agent's built-in tools (Read, Write, Edit, WebFetch) — those run inside the agent's own process.
The Solution: Defense in Depth
twsrt tries to bridge the gap. It reads the same SRT policy that enforces OS-level Bash
restrictions and translates it into application-level rules for every agent's built-in tools:
CANONICAL SOURCES (human-maintained)
====================================
~/.srt-settings.json — OS-level sandbox rules
~/.config/twsrt/bash-rules.json — command deny/ask rules
|
v
+-----------------+
| twsrt | deterministic translation
| (generator) | + drift detection
+--------+--------+
|
+------------+------------+
v v v
Claude Code Copilot CLI (future agents)
settings.json --flag args
ENFORCEMENT LAYERS
==================
Layer 1 (OS): SRT sandbox — kernel-level deny (Bash only)
Layer 2 (App): Agent permissions — tool-level deny/ask (all tools)
This gives you two layers for the most dangerous attack vector (Bash commands accessing credentials or network) and one consistent layer for built-in tools — all generated from a single source of truth.
| Access Path | SRT (Layer 1) | Agent Permissions (Layer 2) | Depth |
|---|---|---|---|
Bash(cat ~/.aws/credentials) |
Kernel-enforced deny | Tool-level deny | Two layers |
Read(~/.aws/credentials) |
Not covered | Tool-level deny | One layer |
Bash(curl evil.com) |
Network proxy blocks | Tool-level deny | Two layers |
WebFetch(evil.com) |
Not covered | Tool-level allow check | One layer |
You then start your agent either with SRT builtin (e.g. claude-code, pi-mono via extenstion) or with srt as
wrapper, e.g. copilot-cli.
srt -c "copilot \
--allow-tool 'shell(*)' \
--allow-tool 'read' \
--allow-tool 'edit' \
--allow-tool 'write' \
--deny-tool 'shell(rm)' \
--deny-tool 'shell(rmdir)' \
--deny-tool 'shell(dd)' \
--deny-tool 'shell(mkfs)' \
...
For the full security analysis and threat model see SECURITY_CONCEPT.md.
Overview
twsrt reads two canonical sources:
- SRT settings (
~/.srt-settings.json) — filesystem read/write deny rules, write allow rules, network domain allowlists - Bash rules (
~/.config/twsrt/bash-rules.json) — command deny/ask rules for Bash execution
It generates security configurations for:
- Claude Code (
~/.claude/settings.json) — permissions.deny, permissions.ask, permissions.allow, sandbox.network - Copilot CLI —
--allow-tooland--deny-toolflag snippets
Key invariant: Source files are never written by twsrt. Target managed sections are never hand-edited.
Installation
# Install as editable uv tool
make install
# Or via pip
pip install twsrt
Usage
Initialize config directory
twsrt init # Creates ~/.config/twsrt/ with config.toml + bash-rules.json
twsrt init --force # Overwrite existing files
Generate agent configs
twsrt generate claude # Print Claude Code permissions to stdout
twsrt generate copilot # Print Copilot CLI flags to stdout
twsrt generate # Generate for all agents
twsrt generate claude --write # Write to ~/.claude/settings.json (selective merge)
twsrt generate claude -n -w # Dry run: show what would be written
Edit canonical sources
twsrt edit srt # Open ~/.srt-settings.json in $EDITOR
twsrt edit bash # Open ~/.config/twsrt/bash-rules.json in $EDITOR
twsrt edit # Show available sources
Detect configuration drift
twsrt diff claude # Compare generated vs existing settings.json
twsrt diff # Check all agents
Exit codes: 0 = no drift, 1 = drift detected, 2 = missing file.
Typical workflow
twsrt edit srt # Add a domain to allowedDomains
twsrt generate claude # Preview the change
twsrt generate claude --write # Apply (selective merge preserves hooks, MCP, etc.)
twsrt diff claude # Verify: exit 0 = no drift
Configuration
SRT is a dependency and needs to be installed separately.
~/.srt-settings.json (SRT — prerequisite)
SRT configuration is the primary canonical source that defines OS-level enforcement boundaries. twsrt reads it to generate matching agent-level rules:
{
"filesystem": {
"denyRead": ["~/.aws", "~/.ssh", "~/.gnupg", "~/.netrc"],
"denyWrite": ["**/.env", "**/*.pem", "**/*.key", "**/secrets/**"],
"allowWrite": [".", "/tmp", "~/dev"]
},
"network": {
"allowedDomains": [
"github.com", "*.github.com",
"pypi.org", "*.pypi.org",
"registry.npmjs.org"
]
}
}
Comprehensive example: .srt-settings.json
~/.config/twsrt/config.toml
[sources]
srt = "~/.srt-settings.json"
bash_rules = "~/.config/twsrt/bash-rules.json"
[targets]
claude_settings = "~/.claude/settings.json"
# copilot_output = "~/.config/twsrt/copilot-flags.txt" # optional, stdout if omitted
~/.config/twsrt/bash-rules.json
{
"deny": ["rm", "sudo", "git push --force"],
"ask": ["git push", "git commit", "pip install"]
}
Comprehensive example: bash-rules.json
Rule Mapping
| SRT / Bash Rule | Claude Code | Copilot CLI |
|---|---|---|
| denyRead directory | Tool(path) + Tool(path/**) in deny | (SRT enforces) |
| denyRead file | Tool(path) in deny | (SRT enforces) |
| denyWrite pattern | Write/Edit/MultiEdit in deny | (SRT enforces) |
| allowWrite path | (no output) | --allow-tool flags |
| allowedDomains domain | WebFetch(domain:X) in allow + sandbox.network | (SRT enforces) |
| Bash deny cmd | Bash(cmd) + Bash(cmd *) in deny | --deny-tool 'shell(cmd)' |
| Bash ask cmd | Bash(cmd) + Bash(cmd *) in ask | --deny-tool (lossy, warns) |
Where Tool = Read, Write, Edit, MultiEdit. Directory vs file detection uses the
filesystem at generation time; glob patterns and unknown paths are treated as
bare patterns (no /** suffix for globs, /** added for unknown paths).
Sandbox Key Mapping
Claude Code's sandbox section has 17 configurable keys. twsrt manages a subset of them
(sourced from .srt-settings.json) and never touches the rest:
| Claude Code Key | SRT Source | Status |
|---|---|---|
sandbox.network.allowedDomains |
network.allowedDomains |
Managed |
sandbox.network.deniedDomains |
network.deniedDomains |
Managed |
sandbox.network.allowLocalBinding |
network.allowLocalBinding |
Managed (pass-through) |
sandbox.network.allowUnixSockets |
network.allowUnixSockets |
Managed (pass-through) |
sandbox.network.allowAllUnixSockets |
network.allowAllUnixSockets |
Managed (pass-through) |
sandbox.network.httpProxyPort |
network.httpProxyPort |
Managed (pass-through) |
sandbox.network.socksProxyPort |
network.socksProxyPort |
Managed (pass-through) |
sandbox.filesystem.allowWrite |
filesystem.allowWrite |
Managed (pass-through) |
sandbox.filesystem.denyWrite |
filesystem.denyWrite |
Managed (pass-through) |
sandbox.filesystem.denyRead |
filesystem.denyRead |
Managed (pass-through) |
sandbox.enabled |
enabled |
Managed (pass-through) |
sandbox.enableWeakerNetworkIsolation |
enableWeakerNetworkIsolation |
Managed (pass-through) |
sandbox.enableWeakerNestedSandbox |
enableWeakerNestedSandbox |
Managed (pass-through) |
sandbox.ignoreViolations |
ignoreViolations |
Managed (pass-through) |
sandbox.excludedCommands |
(no SRT source) | Claude-only — never generated, never removed |
sandbox.autoAllowBashIfSandboxed |
(no SRT source) | Claude-only — never generated, never removed |
sandbox.allowUnsandboxedCommands |
(no SRT source) | Claude-only — never generated, never removed |
Pass-through keys are copied verbatim from SRT to Claude settings without transformation. If a key is absent from SRT, it is omitted from generated output (never set to a default).
Claude-only keys exist only in Claude Code's schema and have no SRT equivalent.
twsrt generate never creates them, and twsrt generate --write preserves them via
selective merge. They are invisible to twsrt.
Merge Behavior (--write)
When writing to ~/.claude/settings.json, twsrt uses selective merge — it does not
overwrite the entire file. Each section has its own merge strategy:
| Section | Strategy | Detail |
|---|---|---|
permissions.deny |
Fully replaced | All existing deny entries removed, replaced with generated ones |
permissions.ask |
Fully replaced | All existing ask entries removed, replaced with generated ones |
permissions.allow |
Selective merge | Only WebFetch(domain:...) entries replaced; everything else preserved |
sandbox.network |
Key-by-key merge | dict.update() — generated keys overwrite, unmanaged keys preserved |
sandbox.filesystem |
Key-by-key merge | dict.update() — generated keys overwrite, unmanaged keys preserved |
sandbox.* (top-level) |
Key-by-key merge | enabled, enableWeaker*, ignoreViolations overwrite; Claude-only keys preserved |
hooks |
Preserved | Untouched |
additionalDirectories |
Preserved | Untouched |
| All other keys | Preserved | Untouched |
What gets preserved in permissions.allow
The allow section is the only one with nuanced logic. twsrt considers WebFetch(domain:...)
entries as "managed" — it strips all existing ones and replaces them with generated ones.
Everything else is treated as user-managed and preserved verbatim:
- Blanket tool allows (
Read,Glob,Grep,LS,Task,WebSearch) — kept - Bash allows (
Bash(npm test:*),Bash(./gradlew:*)) — kept - MCP allows (
mcp__memory__store,mcp__github__search) — kept - Any other custom allows — kept
Implication for Bash commands
twsrt never generates Bash(...) entries in permissions.allow. It only generates Bash
entries in deny and ask (from bash-rules.json). Since those sections are fully
replaced, any manually-added Bash deny/ask entries in settings.json will be lost on
twsrt generate --write. Only entries defined in your bash-rules.json source survive.
However, Bash allow entries you've manually added are safe — they don't match the
WebFetch(domain: prefix and are preserved.
Development
make test # Run tests
make lint # Ruff lint
make format # Ruff format
make ty # Type check with ty
make static-analysis # All of the above
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file twsrt-0.3.1.tar.gz.
File metadata
- Download URL: twsrt-0.3.1.tar.gz
- Upload date:
- Size: 17.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
edc153032812003c7103fbaa282bbf149a9396d24e53766fdb6b5a0d5326784a
|
|
| MD5 |
a0636cd888d9d3a50596563fbc44b805
|
|
| BLAKE2b-256 |
4d37d30196f5d103a22c89a4399c76a3ffe7a63e5d35c8deee6cff96487bb167
|
File details
Details for the file twsrt-0.3.1-py3-none-any.whl.
File metadata
- Download URL: twsrt-0.3.1-py3-none-any.whl
- Upload date:
- Size: 15.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a9d346bf1615b8b30b0984524ae35d7d224724d1d87db5b5a67c0fad6d07559
|
|
| MD5 |
fc3f34d26d6c324bbd4ea30f1c3449ee
|
|
| BLAKE2b-256 |
dca5fc31e16fd11511d655ff7fc907b4f8ef3162b772b68572c1942a7718a144
|