Documentation Index Fetch the complete documentation index at: https://mintlify.com/nullclaw/nullclaw/llms.txt
Use this file to discover all available pages before exploring further.
Overview
NullClaw enforces security at every layer of the stack:
Gateway pairing — One-time code authentication
Channel allowlists — Sender validation
Workspace scoping — Filesystem boundaries
Sandbox isolation — OS-level containment
Encrypted secrets — ChaCha20-Poly1305 AEAD
Audit logging — Signed event trail
Resource limits — Memory, CPU, disk quotas
Security Table
# Item Status How 1 Gateway not publicly exposed Done Binds 127.0.0.1 by default. Refuses 0.0.0.0 without tunnel or explicit allow_public_bind. 2 Pairing required Done 6-digit one-time code on startup. Exchange via POST /pair for bearer token. 3 Filesystem scoped Done workspace_only = true by default. Null byte injection blocked. Symlink escape detection.4 Access via tunnel only Done Gateway refuses public bind without active tunnel. Supports Tailscale, Cloudflare, ngrok, or custom. 5 Sandbox isolation Done Auto-detects best backend: Landlock, Firejail, Bubblewrap, or Docker. 6 Encrypted secrets Done API keys encrypted with ChaCha20-Poly1305 using local key file. 7 Resource limits Done Configurable memory, CPU, disk, and subprocess limits. 8 Audit logging Done Signed event trail with configurable retention.
1. Gateway Pairing
How It Works
Configuration
{
"gateway" : {
"port" : 3000 ,
"require_pairing" : true ,
"allow_public_bind" : false ,
"pairing_code_ttl_secs" : 300 // 5 minutes
}
}
Pairing Flow
Start gateway :
nullclaw gateway
# → Pairing code: 123456
Client pairs :
curl -X POST http://127.0.0.1:3000/pair \
-H "X-Pairing-Code: 123456"
# → {"token": "tok_abc123..."}
Use bearer token :
curl http://127.0.0.1:3000/webhook \
-H "Authorization: Bearer tok_abc123..." \
-d '{"message": "Hello"}'
Pairing codes are single-use and expire after 5 minutes (configurable). Bearer tokens persist until revoked.
2. Channel Allowlists
Every channel enforces a sender allowlist :
{
"channels" : {
"telegram" : {
"accounts" : {
"main" : {
"allow_from" : [ "alice" , "bob" ]
}
}
}
}
}
Allowlist Rules
Empty allowlist ([]) → Deny all inbound messages
Wildcard (["*"]) → Allow all (explicit opt-in)
Exact match → Case-insensitive username/ID match
Special Cases
Nostr
The owner_pubkey is always allowed regardless of dm_allowed_pubkeys:
{
"channels" : {
"nostr" : {
"owner_pubkey" : "npub1abc..." ,
"dm_allowed_pubkeys" : [ "npub1def..." , "npub1ghi..." ]
}
}
}
Signal
Supports phone numbers and UUIDs:
{
"channels" : {
"signal" : {
"allow_from" : [ "+1234567890" , "uuid:a1b2c3d4-..." ]
}
}
}
3. Workspace Scoping
Default Behavior
All file operations are restricted to ~/.nullclaw/workspace/ by default:
{
"autonomy" : {
"workspace_only" : true ,
"allowed_paths" : [] // Additional paths outside workspace
}
}
Path Validation
pub fn validatePath (
path : [] const u8 ,
workspace_dir : [] const u8 ,
allowed_paths : [] const [] const u8 ,
workspace_only : bool ,
) ! void {
// 1. Null byte injection blocked
if ( std . mem . indexOfScalar ( u8 , path , 0 ) != null ) {
return error . InvalidPath ;
}
// 2. Resolve to absolute path
const abs_path = try std . fs . realpathAlloc ( allocator , path );
defer allocator . free ( abs_path );
// 3. Check workspace boundary
if ( workspace_only ) {
if ( ! std . mem . startsWith ( u8 , abs_path , workspace_dir )) {
// Check additional allowed paths
for ( allowed_paths ) | allowed | {
if ( std . mem . startsWith ( u8 , abs_path , allowed )) {
return ; // Allowed
}
}
return error . PathOutsideWorkspace ;
}
}
// 4. Symlink escape detection (handled by realpathAlloc)
}
Expanding Access
Allow specific paths outside workspace:
{
"autonomy" : {
"workspace_only" : true ,
"allowed_paths" : [
"/home/user/projects/myproject" ,
"/var/log/app"
]
}
}
Or disable entirely (not recommended):
{
"autonomy" : {
"workspace_only" : false
}
}
Disabling workspace scoping allows the agent to read/write any file the process user has access to. Use with caution.
4. Sandbox Isolation
Supported Backends
Backend OS Method Capabilities Landlock Linux 5.13+ Kernel LSM Filesystem access control, syscall filtering Firejail Linux Seccomp + namespaces Full process isolation, network blocking Bubblewrap Linux User namespaces Lightweight containerization Docker Linux/macOS Container runtime Full isolation, custom images
Configuration
{
"security" : {
"sandbox" : {
"backend" : "auto" // auto | landlock | firejail | bubblewrap | docker | none
}
}
}
Auto-detection order : Landlock → Firejail → Bubblewrap → Docker → None (fallback)
Docker Sandbox
{
"runtime" : {
"kind" : "docker" ,
"docker" : {
"image" : "alpine:3.20" ,
"network" : "none" ,
"memory_limit_mb" : 512 ,
"read_only_rootfs" : true
}
}
}
Restrictions:
No network by default
Read-only rootfs (workspace mounted as writable volume)
Memory limit enforced
Auto-removed on exit
Landlock Example
const sandbox = try LandlockSandbox . init ( allocator , .{
. allowed_read_dirs = & .{ workspace_dir },
. allowed_write_dirs = & .{ workspace_dir },
});
defer sandbox . deinit ();
try sandbox . enable ();
// Now process is restricted to workspace_dir only
5. Encrypted Secrets
SecretStore
API keys are encrypted at rest with ChaCha20-Poly1305 AEAD :
const SecretStore = struct {
key_file_path : [] const u8 ,
pub fn encrypt (
self : SecretStore ,
allocator : std . mem . Allocator ,
plaintext : [] const u8 ,
) ! [] const u8 {
// Returns "enc2:<hex-ciphertext>"
}
pub fn decrypt (
self : SecretStore ,
allocator : std . mem . Allocator ,
ciphertext : [] const u8 ,
) ! [] const u8 {
// Parses "enc2:..." prefix, decrypts, verifies tag
}
};
Configuration
{
"secrets" : {
"encrypt" : true ,
"key_file" : "~/.nullclaw/secret.key" // Auto-generated if missing
}
}
Key Management
Key file format: 32-byte random key (256-bit)
# Auto-generated on first run
ls -l ~/.nullclaw/secret.key
# -rw------- 1 user user 32 Mar 1 12:34 secret.key
Do NOT commit secret.key to git . Backup securely. Loss of this file means encrypted secrets cannot be decrypted.
Encrypted Config Fields
{
"models" : {
"providers" : {
"openai" : {
"api_key" : "enc2:a1b2c3d4..."
}
}
},
"channels" : {
"nostr" : {
"private_key" : "enc2:e5f6g7h8..."
}
}
}
Decrypted at runtime, never written back in plaintext.
6. Audit Logging
AuditEvent Structure
pub const AuditEvent = struct {
timestamp : i64 ,
event_type : AuditEventType ,
actor : Actor ,
action : Action ,
result : ExecutionResult ,
context : SecurityContext ,
signature : ? [] const u8 = null , // HMAC-SHA256 signature
};
Configuration
{
"security" : {
"audit" : {
"enabled" : true ,
"retention_days" : 90 ,
"log_file" : "~/.nullclaw/audit.log" ,
"sign_events" : true
}
}
}
Example Log Entry
{
"timestamp" : 1709299200 ,
"event_type" : "command_execution" ,
"actor" : {
"user_id" : "alice" ,
"channel" : "telegram" ,
"session_id" : "sess_abc123"
},
"action" : {
"type" : "shell" ,
"command" : "ls -la" ,
"workspace_dir" : "/home/user/.nullclaw/workspace"
},
"result" : {
"success" : true ,
"exit_code" : 0
},
"signature" : "hmac-sha256:a1b2c3..."
}
Querying Logs
# Show recent audit events
tail -f ~/.nullclaw/audit.log | jq
# Filter by event type
jq 'select(.event_type == "command_execution")' ~/.nullclaw/audit.log
# Filter by actor
jq 'select(.actor.user_id == "alice")' ~/.nullclaw/audit.log
7. Resource Limits
Configuration
{
"security" : {
"resources" : {
"max_memory_mb" : 512 ,
"max_cpu_percent" : 80 ,
"max_disk_usage_mb" : 1024 ,
"max_subprocesses" : 10
}
}
}
{
"tools" : {
"shell" : {
"timeout_secs" : 60 ,
"max_output_bytes" : 100000
},
"file_read" : {
"max_file_size_bytes" : 10485760 // 10 MB
},
"http_request" : {
"timeout_secs" : 30 ,
"max_response_size" : 1000000 // 1 MB
}
}
}
Enforcement
Shell timeout : Subprocess killed after N seconds
Output truncation : STDOUT/STDERR capped at max_output_bytes
File size : file_read fails if file > max_file_size_bytes
HTTP response : Downloads stopped after max_response_size bytes
Command Risk Classification
Risk Levels
pub const CommandRiskLevel = enum {
low , // ls, cat, echo, git status
medium , // curl, wget, chmod +x, npm install
high , // rm -rf, dd, mkfs, shutdown, reboot
};
High-Risk Commands
Blocked by default (configurable):
rm -rf
dd if=/dev/zero
mkfs
shutdown
reboot
:(){ :|:& };: (fork bomb)
Configuration
{
"autonomy" : {
"level" : "supervised" , // full | supervised | restricted
"allowed_commands" : [ "git" , "npm" , "zig" ], // Prefix matching
"block_high_risk_commands" : true ,
"require_approval_for_medium_risk" : false
}
}
Autonomy levels :
full — Allow all commands (ignores allowed_commands)
supervised — Allow allowed_commands, block high-risk
restricted — Allow allowed_commands only, require approval for medium-risk
Next Steps
Configuration Full security configuration reference
Architecture Learn about the security-first design