A policy-based gatekeeper service that sits between AI agents and real-world tools (shell, HTTP, filesystem), enforcing approvals, denials, USD budgets, and audit logging.
If you're running AI agents (Claude, GPT, local LLMs, MCP servers) that execute tool calls against real systems — and you're responsible for what happens when those calls go wrong — Gatekeeper is for you. Specifically:
- You build agentic apps and your agent can run shell commands, write files, or hit external APIs.
- You've already had one "why did it do that?" moment — or you're staring down the possibility.
- You want a self-hostable, auditable, single-binary boundary between your agent and the world, without adopting an enterprise SSO stack or wiring everything through a cloud proxy.
- You need USD budgets and signed approvals as first-class primitives, not features bolted onto an observability tool.
You were probably using: nothing (and trusting the prompt), ad-hoc Python wrappers around subprocess, a cloud LLM gateway, or shell-level sudoers rules that don't understand tool semantics.
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
- Catches sensitive-boundary crossings — Keychain, SSH keys, cloud credentials, browser profiles, package-registry tokens, env files (see Sensitive Boundary Protection)
- Rejects when a USD / token / call budget is exceeded (optional) — per actor (a rolling guardrail) or per run (
scope: run, keyed onrunId: caps a single agentic run at the action boundary, where recursive burn compounds) - Optionally proxies Anthropic model calls — route inference through the gatekeeper so every
/v1/messagescall is audited, the API key stays centralized, and real per-token cost is metered onto the audit row and into budgets (off by default; see docs/API.md)
All decisions are logged to an append-only audit trail (jsonl or Postgres). An aggregation endpoint (/usage) exposes call counts — and real cost/token sums — by actor × tool × day. A budget endpoint (/budget) surfaces current spend vs cap per configured rule.
Coding agents fail in subtle ways. They start with a safe task, then escalate into sensitive local operations while trying to be helpful. Gatekeeper ships a built-in rule pack that catches these crossings.
Real failure pattern: A coding agent debugging a Puppeteer / Chromium
"Safe Storage" prompt on macOS first applied a normal flag-based fix
(--use-mock-keychain, --password-store=basic, throwaway --user-data-dir).
When the prompt persisted, the agent pivoted to inspecting the user's
Keychain — and proposed deleting entries "so there's nothing to access."
# Step 2 of the agent's reasoning — Gatekeeper requires approval (high risk)
security find-generic-password -s "Chromium Safe Storage"
# Step 3 of the agent's reasoning — Gatekeeper denies outright (critical)
security delete-generic-password -s "Chromium Safe Storage"Gatekeeper classifies these against a built-in rule pack:
| Resource class | Examples |
|---|---|
credential_store |
macOS Keychain (find / dump / delete) |
private_key |
~/.ssh/id_*, *.pem, ~/.ssh directory enumeration |
cloud_credentials |
~/.aws/credentials, ~/.config/gcloud/, ~/.azure/ |
env_secret |
cat .env, .env.* reads via shell |
package_registry_token |
~/.npmrc, ~/.pypirc |
developer_auth |
~/.git-credentials, gh auth token |
browser_profile |
Chrome / Chromium / Brave / Arc / Firefox profile dirs |
unknown_sensitive |
Broad recursive home-dir secret greps |
Each rule has an effect (allow | require_approval | deny), a stable
category, a risk level, and an optional safer_alternative redirect.
Rules are mirrored into riskFlags (boundary:keychain-read,
category:credential_store_access, risk:high) so existing audit consumers
work unchanged.
Defaults always load — no opt-in required. Override or extend them under
sensitive_boundaries: in your policy.yaml; the full reference dump lives
at policies/sensitive-boundaries.yaml.
A demo fixture for the Puppeteer → Keychain escalation is at
examples/sensitive-boundaries/keychain-scope-creep.json.
@runestone-labs/gatekeeper-claude-code is a
Claude Code PreToolUse hook that routes Bash / Write / Edit / WebFetch
through Gatekeeper before Claude Code executes them. Install once globally,
drop the settings.example.json
snippet into ~/.claude/settings.json, and every gated tool call gets
evaluated against the boundary pack:
npm install -g @runestone-labs/gatekeeper-claude-code{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|WebFetch",
"hooks": [{ "type": "command", "command": "gatekeeper-claude-code-hook" }]
}
]
}
}Fail-open by default if the Gatekeeper server is down; set
GATEKEEPER_FAIL_CLOSED=1 to flip to fail-closed. See
integrations/claude-code/README.md
for the full configuration reference.
@runestone-labs/gatekeeper-mcp is a stdio MCP
server that lets any MCP client (Claude Desktop, Claude Code, Cursor, …) run
real tool calls — shell_exec, files_write, http_request, a generic
gatekeeper_call, plus gatekeeper_health — but only by routing every call
through Gatekeeper. Identity, role, and origin are pinned server-side (the
model can't escalate via tool arguments), and it fails closed: a deny, a
pending approval, or any malformed Gatekeeper response surfaces as an error,
never as a successful result.
Add it to your MCP client config:
{
"mcpServers": {
"gatekeeper": {
"command": "npx",
"args": ["-y", "@runestone-labs/gatekeeper-mcp"],
"env": {
"GATEKEEPER_URL": "http://127.0.0.1:3847",
"GATEKEEPER_ROLE": "agent"
}
}
}
}It's also listed on the MCP Registry (preview) as
io.github.runestone-labs/gatekeeper. See
integrations/mcp-server/README.md for
the full tool table and security model.
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
npm run bootstrap # generates .env with a fresh GATEKEEPER_SECRET
docker compose upnpm run bootstrap creates .env from .env.example and writes a random 48-character GATEKEEPER_SECRET into it. Skip if you already have a .env you want to keep — docker-compose will error clearly if the secret isn't set.
Gatekeeper 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:
# requestId must be a UUID
# 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":"'"$(uuidgen)"'","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":"'"$(uuidgen)"'","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"
# Optional: Route Anthropic model calls through the gatekeeper (default: off)
# When enabled, point your SDK's ANTHROPIC_BASE_URL at <gatekeeper>/anthropic.
# See docs/API.md for the full /anthropic proxy reference.
export ENABLE_ANTHROPIC_PROXY=false
# Optional: Anthropic API key. When set, the /anthropic proxy injects it on
# proxied calls so the key can live only in gatekeeper (not in the agent).
export ANTHROPIC_API_KEY="sk-ant-..."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.2",
"policyHash": "sha256:abc123...",
"uptime": 3600,
"pendingApprovals": 2,
"demoMode": false,
"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.3.2"
}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/POLICY_GUIDE.md - How to write and customize policies (including budgets)
- docs/APPROVALS.md - Approval workflow details and troubleshooting
- docs/AUDIT_LOGS.md - Audit log format and querying
- docs/MEMORY.md - Graph-based memory system setup and API reference
- docs/KG_PATTERNS.md - Practical patterns for using the memory module (entities, episodes, evidence, provenance conventions)
- docs/DEPLOY.md - Production deployment + non-obvious operational behaviors
- 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.