A policy-based gatekeeper service that sits between AI agents and real-world tools (shell, HTTP, filesystem), enforcing approvals, denials, and audit logging.
AI agents need to execute actions in the real world: running shell commands, writing files, making HTTP requests. Without guardrails, an agent can accidentally (or adversarially) execute dangerous operations.
The Gatekeeper intercepts all tool requests and:
- Allows low-risk operations immediately
- Denies operations that match dangerous patterns
- Requires human approval for sensitive operations
All decisions are logged to an append-only audit trail.
This gatekeeper protects against:
- Accidental damage: Agent runs
rm -rf /or overwrites critical files - Prompt injection execution: Malicious content tricks agent into dangerous actions
- Exfiltration: Agent sends secrets to external services
- SSRF attacks: Agent accesses internal services via HTTP
This gatekeeper does NOT protect against:
- Malicious operator with access to the policy file
- Attacks on the gatekeeper service itself
- Social engineering of the human approver
- Denial of service (no rate limiting)
The fastest way to try Gatekeeper:
git clone https://github.com/Runestone-Labs/gatekeeper.git
cd gatekeeper
docker-compose upGatekeeper is now running at http://127.0.0.1:3847 with demo mode enabled.
Test it with the quickstart script (walks through DENY, ALLOW, and APPROVE):
bash examples/quickstart.shOr test individual decisions:
# This will be DENIED (dangerous pattern)
curl -s -X POST http://127.0.0.1:3847/tool/shell.exec \
-H "Content-Type: application/json" \
-d '{"requestId":"test-001","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"command":"rm -rf /"}}'
# This will be ALLOWED
curl -s -X POST http://127.0.0.1:3847/tool/http.request \
-H "Content-Type: application/json" \
-d '{"requestId":"test-002","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"url":"https://httpbin.org/get","method":"GET"}}'To customize policy:
cp policy.example.yaml policy.yaml
# Edit policy.yaml, then update docker-compose.yaml volume to use ./policy.yaml
docker-compose restartFor manual installation without Docker, see below.
npm install# Required: Secret for HMAC signing (at least 32 characters)
export GATEKEEPER_SECRET="your-secret-key-at-least-32-chars-long"
# Provider selection (optional)
export APPROVAL_PROVIDER=local # local | slack | runestone (default: local)
export AUDIT_SINK=jsonl # jsonl | runestone (default: jsonl)
export POLICY_SOURCE=yaml # yaml | runestone (default: yaml)
# Optional: Slack webhook for approval notifications (when using slack provider)
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
# Optional: Custom port (default: 3847)
export GATEKEEPER_PORT=3847
# Optional: Bind host (default: 127.0.0.1)
# Use 0.0.0.0 when running in Docker.
export GATEKEEPER_HOST=127.0.0.1
# Optional: Base URL for approval links
export BASE_URL="http://127.0.0.1:3847"cp policy.example.yaml policy.yaml
# Edit policy.yaml to match your requirementsnpm start
# Or for development with auto-reload:
npm run devSee all three decision types in action:
# Install dependencies
npm install
# Set a demo secret (or use your own)
export GATEKEEPER_SECRET="demo-secret-at-least-32-characters-long"
# Run the demo
npm run demoThe demo runs through:
- DENY - Dangerous command (
rm -rf /) is blocked - APPROVE - Safe command (
ls -la) requires approval, then auto-approved - ALLOW - HTTP request executes immediately
# Record with asciinema (creates demo.cast)
npm run demo:record
# Playback
asciinema play demo.cast
# Create GIF/MP4 with VHS (requires: brew install vhs)
npm run demo:gifdemo.cast- Terminal recording (asciinema format)demo.gif- Animated GIF for sharingdemo.mp4- Video filedata/audit/YYYY-MM-DD.jsonl- Audit log with all demo actions
The gatekeeper uses a pluggable provider system for flexibility:
- local (default): Logs approval URLs to console
- slack: Sends interactive approval requests via Slack webhook
- runestone: Enterprise control plane (coming soon)
- jsonl (default): Writes to daily JSONL files in
data/audit/ - runestone: Stream to cloud for search and compliance (coming soon)
- yaml (default): Load from local YAML file
- runestone: Managed policies with version control (coming soon)
All tool requests must include actor.role to enforce principal policies. For safe retries, include an idempotencyKey.
curl -X POST http://127.0.0.1:3847/tool/http.request \
-H "Content-Type: application/json" \
-d '{
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"actor": {
"type": "agent",
"name": "my-agent",
"role": "openclaw",
"runId": "run-123"
},
"args": {
"url": "https://api.example.com/data",
"method": "GET"
}
}'Response (200):
{
"decision": "allow",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"reasonCode": "POLICY_ALLOW",
"humanExplanation": "Policy allows \"http.request\".",
"policyVersion": "sha256:abc123...",
"success": true,
"result": {
"status": 200,
"headers": {"content-type": "application/json"},
"body": "{...}"
},
"executionReceipt": {
"startedAt": "2024-01-15T10:30:00.000Z",
"completedAt": "2024-01-15T10:30:00.120Z",
"durationMs": 120
}
}curl -X POST http://127.0.0.1:3847/tool/shell.exec \
-H "Content-Type: application/json" \
-d '{
"requestId": "550e8400-e29b-41d4-a716-446655440001",
"actor": {
"type": "agent",
"name": "my-agent",
"role": "openclaw"
},
"args": {
"command": "ls -la /tmp"
}
}'Response (202):
{
"decision": "approve",
"requestId": "550e8400-e29b-41d4-a716-446655440001",
"approvalId": "abc123...",
"expiresAt": "2026-01-31T13:00:00.000Z",
"reasonCode": "POLICY_APPROVAL_REQUIRED",
"humanExplanation": "Policy requires human approval before running \"shell.exec\".",
"message": "Approval required. Check local for approval links.",
"approvalRequest": {
"approvalId": "abc123...",
"expiresAt": "2026-01-31T13:00:00.000Z",
"reasonCode": "POLICY_APPROVAL_REQUIRED",
"humanExplanation": "Policy requires human approval before running \"shell.exec\"."
},
"policyVersion": "sha256:abc123..."
}curl -X POST http://127.0.0.1:3847/tool/shell.exec \
-H "Content-Type: application/json" \
-d '{
"requestId": "550e8400-e29b-41d4-a716-446655440002",
"actor": {
"type": "agent",
"name": "my-agent",
"role": "openclaw"
},
"args": {
"command": "rm -rf /"
}
}'Response (403):
{
"decision": "deny",
"requestId": "550e8400-e29b-41d4-a716-446655440002",
"reasonCode": "TOOL_DENY_PATTERN",
"humanExplanation": "Request matches a deny pattern configured for this tool.",
"policyVersion": "sha256:abc123..."
}If a tool is configured with decision: approve, you can pre-authorize a specific call
with a capability token scoped to tool + args hash:
npm run capability:create -- --tool shell.exec --args /tmp/args.json --ttl 3600Include the capabilityToken in the tool request. Gatekeeper will allow the call
without manual approval if the token is valid.
curl http://127.0.0.1:3847/healthResponse:
{
"version": "0.3.0",
"policyHash": "sha256:abc123...",
"uptime": 3600,
"pendingApprovals": 2,
"providers": {
"approval": "local",
"policy": "yaml"
},
"database": {
"available": true,
"healthy": true,
"latencyMs": 2
},
"memory": {
"enabled": true
}
}See policy.example.yaml for a complete example.
tools:
shell.exec:
decision: approve # allow | approve | deny
deny_patterns:
- "rm -rf" # Regex patterns to block
allowed_cwd_prefixes:
- "/tmp/" # Allowed working directories
allowed_commands:
- "ls"
- "git"
sandbox_command_prefix:
- "firejail"
- "--noprofile"
- "--"
max_output_bytes: 1048576
max_timeout_ms: 30000
files.write:
decision: approve
allowed_paths:
- "/tmp/"
deny_extensions:
- ".env"
max_size_bytes: 10485760
http.request:
decision: allow
allowed_methods: ["GET", "POST"]
allowed_domains:
- "api.example.com"
deny_domains:
- "pastebin.com"
deny_ip_ranges: # SSRF protection
- "127.0.0.0/8"
- "169.254.0.0/16"
max_body_bytes: 1048576
max_redirects: 3For a complete policy writing tutorial, see docs/POLICY_GUIDE.md.
- Agent submits tool request
- Gatekeeper evaluates against policy
- If
approve: Creates pending approval, sends notification via configured provider - Human clicks Approve or Deny link
- If Approved: Tool executes, result returned
- All actions logged to audit trail
Approval links are:
- HMAC-signed (tamper-proof)
- Single-use (prevents replay)
- Time-limited (1 hour expiry)
For a detailed approval workflow guide, see docs/APPROVALS.md.
All requests are logged via the configured audit sink. Default (jsonl) writes to data/audit/YYYY-MM-DD.jsonl:
{
"timestamp": "2026-01-31T12:00:00.000Z",
"requestId": "550e8400-...",
"tool": "shell.exec",
"decision": "approve",
"actor": {"type": "agent", "name": "my-agent", "role": "openclaw"},
"argsSummary": "{\"command\":\"ls -la\"}",
"riskFlags": [],
"policyHash": "sha256:abc123...",
"gatekeeperVersion": "0.1.0"
}Logs are:
- Append-only (never modified)
- One file per day (easy rotation)
- Include policy hash (for forensics)
- Secrets are redacted
For a complete audit log reference with querying examples, see docs/AUDIT_LOGS.md.
Install the TypeScript client for integrating your agent with Gatekeeper:
npm install @runestone-labs/gatekeeper-clientimport { GatekeeperClient } from '@runestone-labs/gatekeeper-client';
const client = new GatekeeperClient({
baseUrl: 'http://127.0.0.1:3847',
role: 'openclaw',
});
// Execute a shell command through the gatekeeper
const result = await client.shellExec({ command: 'ls -la' });
console.log(result.decision); // 'allow' | 'approve' | 'deny'See the full client README for all available methods.
Gatekeeper is designed to be agent-agnostic. Any agent that can route tool calls over HTTP can integrate with Gatekeeper. See INTEGRATING_AGENTS.md for the integration pattern.
Runestone Control Plane provides:
- Managed Policies: Version-controlled policy configuration with templates
- Searchable Audit: Full-text search across all audit logs with compliance exports
- Web-based Approvals: Modern approval UI with mobile notifications
- Team Workflows: Approval routing, escalation, and delegation
Contact: enterprise@runestone.dev
| Feature | Implementation | Rationale |
|---|---|---|
| Approval signing | HMAC-SHA256 of full payload | Prevents parameter tampering |
| Single-use approvals | Status field + atomic update | Prevents replay attacks |
| Expiry | 1 hour default | Limits approval window |
| Input validation | Zod with .strict() |
Rejects unknown fields |
| Shell constraints | cwd allowlist, timeout caps | Limits blast radius |
| SSRF protection | DNS resolution + IP checks | Blocks internal access |
| Audit logging | Append-only via pluggable sink | Tamper-evident trail |
# Type check
npm run typecheck
# Run tests
npm run test:run
# Run with auto-reload
npm run dev
# Run production
npm startThe memory module provides graph-based knowledge storage (entities, episodes, evidence) via PostgreSQL + Apache AGE. It is an optional module — Gatekeeper works as a standalone policy engine without it.
Without a database: Only core tools (shell.exec, files.write, http.request) are registered. Policy enforcement, approvals, and JSONL audit logging work normally.
With a database: Memory tools are additionally registered, providing a knowledge graph for AI assistants.
# Set DATABASE_URL to enable the memory module
export DATABASE_URL="postgresql://user:pass@localhost:5432/memory"
# Or explicitly control (overrides DATABASE_URL detection)
export ENABLE_MEMORY=true # or false to disable even with a DATABASE_URL# Generate migration SQL from schema changes
npm run db:generate
# Apply migrations to a running database
npm run db:migrate
# Or push schema directly (dev only)
npm run db:push| Tool | Description |
|---|---|
memory.upsert |
Create/update entities (people, projects, concepts) |
memory.link |
Create relationships between entities |
memory.unlink |
Remove relationships between entities |
memory.query |
Query entities (with full-text search) and traverse relationships |
memory.episode |
Log decisions, events, and observations |
memory.evidence |
Attach evidence/provenance to entities or episodes |
# Create an entity
curl -X POST http://127.0.0.1:3847/tool/memory.upsert \
-H "Content-Type: application/json" \
-d '{"requestId":"...","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"type":"person","name":"Alice"}}'
# Link two entities
curl -X POST http://127.0.0.1:3847/tool/memory.link \
-H "Content-Type: application/json" \
-d '{"requestId":"...","actor":{"type":"agent","name":"test","role":"openclaw"},"args":{"sourceId":"<id1>","targetId":"<id2>","relation":"knows"}}'The database schema is split into two modules:
src/db/schema/audit.ts— Audit logs table (core gatekeeper, always available)src/db/schema/memory.ts— Knowledge graph tables: entities, episodes, evidence (optional module)
The KG schema is a generic entity/episode/evidence model. Application-specific ontology (entity types, facet types, edge relations) is defined by the consuming application, not by gatekeeper.
See docs/MEMORY.md for setup and full API reference.
- docs/MEMORY.md - Graph-based memory system setup and usage
- docs/POLICY_GUIDE.md - How to write and customize policies
- docs/APPROVALS.md - Approval workflow details and troubleshooting
- docs/AUDIT_LOGS.md - Audit log format and querying
- THREAT_MODEL.md - Security assumptions and non-goals
- INTEGRATING_AGENTS.md - Using Gatekeeper with real agents
- RUNESTONE_CLOUD.md - OSS vs Cloud architecture
- CONTRIBUTING.md - How to contribute
- SECURITY.md - Security policy and vulnerability reporting
- GOVERNANCE.md - Project governance
- CHANGELOG.md - Release history
Apache-2.0 - See LICENSE for details.