Skip to content

Runestone-Labs/gatekeeper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

105 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Runestone Agent Gatekeeper

CI npm License

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.

Who Is This For

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.

What Problem This Solves

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 on runId: 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/messages call 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.

Sensitive Boundary Protection

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.

Use it from Claude Code

@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.

Use it from any MCP client

@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.

Threat Model

This gatekeeper protects against:

  1. Accidental damage: Agent runs rm -rf / or overwrites critical files
  2. Prompt injection execution: Malicious content tricks agent into dangerous actions
  3. Exfiltration: Agent sends secrets to external services
  4. 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)

Quick Start with Docker

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 up

npm 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.sh

Or 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 restart

For manual installation without Docker, see below.

Quick Start (Manual)

1. Install Dependencies

npm install

2. Configure Environment

# 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-..."

3. Create Policy File

cp policy.example.yaml policy.yaml
# Edit policy.yaml to match your requirements

4. Start the Server

npm start
# Or for development with auto-reload:
npm run dev

Demo (2 minutes)

See 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 demo

The demo runs through:

  1. DENY - Dangerous command (rm -rf /) is blocked
  2. APPROVE - Safe command (ls -la) requires approval, then auto-approved
  3. ALLOW - HTTP request executes immediately

Recording

# 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:gif

Outputs

  • demo.cast - Terminal recording (asciinema format)
  • demo.gif - Animated GIF for sharing
  • demo.mp4 - Video file
  • data/audit/YYYY-MM-DD.jsonl - Audit log with all demo actions

Provider Architecture

The gatekeeper uses a pluggable provider system for flexibility:

Approval Providers

  • local (default): Logs approval URLs to console
  • slack: Sends interactive approval requests via Slack webhook
  • runestone: Enterprise control plane (coming soon)

Audit Sinks

  • jsonl (default): Writes to daily JSONL files in data/audit/
  • runestone: Stream to cloud for search and compliance (coming soon)

Policy Sources

  • yaml (default): Load from local YAML file
  • runestone: Managed policies with version control (coming soon)

Example Requests

All tool requests must include actor.role to enforce principal policies. For safe retries, include an idempotencyKey.

Execute a Tool (Allow Decision)

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
  }
}

Execute a Tool (Approve Decision)

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..."
}

Execute a Tool (Deny Decision)

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..."
}

Capability Tokens (Pre-Approved)

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 3600

Include the capabilityToken in the tool request. Gatekeeper will allow the call without manual approval if the token is valid.

Health Check

curl http://127.0.0.1:3847/health

Response:

{
  "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
  }
}

Policy Configuration

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: 3

For a complete policy writing tutorial, see docs/POLICY_GUIDE.md.

Approval Flow

  1. Agent submits tool request
  2. Gatekeeper evaluates against policy
  3. If approve: Creates pending approval, sends notification via configured provider
  4. Human clicks Approve or Deny link
  5. If Approved: Tool executes, result returned
  6. 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.

Audit Logs

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.

Client Library

Install the TypeScript client for integrating your agent with Gatekeeper:

npm install @runestone-labs/gatekeeper-client
import { 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.

Using with Real Agents

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.

Enterprise Control Plane

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

Security Decisions

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

Development

# Type check
npm run typecheck

# Run tests
npm run test:run

# Run with auto-reload
npm run dev

# Run production
npm start

Memory Module (Optional)

The 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.

Configuration

# 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

Database Setup

# 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

Memory Tools

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"}}'

Schema Architecture

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.

Documentation

Guides

Reference

Contributing

License

Apache-2.0 - See LICENSE for details.