Resource-level permissions for AI agents.
Wombat sits between your AI agent and its tools. Every tool call is checked against a permissions policy. Deny by default. Allow by declaration.
AI agents are powerful. They can push code, delete branches, read secrets. When you give an agent access to your GitHub org or production database, you're trusting it with a lot.
Current permission systems are blunt: you can allow or deny entire tools. But the same push_files call should be allowed on feature/x and denied on main. The same read_file call should work on your project but not on ~/.aws/credentials.
Wombat solves this. It's the Unix chmod for AI agents: rwxd on any resource, deny by default, most specific rule wins.
Fork usewombat/gateway-demo, clone your fork, and run claude inside it. The repo is self-contained — it detects your fork automatically and wires up Wombat with no manual configuration.
You'll see the agent allowed to trigger a CI workflow and denied when it tries to push to main.
npm install -g @usewombat/gatewayRequires Node.js 22+. If you use the github upstream, Docker must also be installed — it runs via ghcr.io/github/github-mcp-server.
Wombat's permission engine is published as a standalone library — use it in your own MCP proxy, hook server, or CLI:
npm install @usewombat/gatewayimport { checkPermission, resolve, findGrant, parseManifest } from "@usewombat/gateway";
// Load or create a manifest
const manifest = parseManifest({
version: "0.1",
umask: "----",
grants: [
{ resource: "github/org/repo", mode: "r---" },
],
sudo: { enabled: false },
});
// Resolve a tool call to a resource path
const resolved = resolve({ tool: "github__push_files", params: { owner: "org", repo: "repo" } });
// → { resource: "github/org/repo", mode: "w" }
// Check against permissions
const result = checkPermission({ tool: "github__push_files", params: {} }, manifest);
// → { decision: "deny", matchedGrant: null, resource: "github/org/repo", mode: "w", umask: "----" }The library has zero dependencies beyond TypeScript types — the permission engine is pure functions with no I/O.
| Import | From | Pure? |
|---|---|---|
checkPermission() |
gateway.ts |
✅ — deterministic, no side effects |
resolve() |
resolver.ts |
✅ — deterministic |
findGrant() |
manifest.ts |
✅ — specificity scoring |
parseManifest() |
manifest.ts |
✅ — Zod-validated |
parseMode() / modeAllows() |
manifest.ts |
✅ — mode helpers |
buildDenyResponse() |
gateway.ts |
✅ — builds MCP error shape |
hashManifestFile() |
manifest.ts |
✅ — sha256 of manifest file |
AuditEnvelope |
types.ts |
Type — structured check result |
Wombat has two modes. Pick one.
cp $(wombat --example) ./permissions.json
# edit permissions.jsonExample: a CI/deploy agent that can trigger workflows and push to feature branches, but can never touch main directly:
{
"version": "0.1",
"umask": "----",
"grants": [
{ "resource": "github/my-org/my-repo", "mode": "r-x-", "comment": "Read repo, trigger CI workflows" },
{ "resource": "github/my-org/my-repo/main", "mode": "r---", "comment": "Main is read-only — no direct pushes" },
{ "resource": "github/my-org/my-repo/feature/*", "mode": "rw--", "comment": "Push to feature branches" }
],
"sudo": { "enabled": false }
}For Claude Code:
claude mcp add wombat -- wombat \
--manifest ~/my-project/permissions.json \
--upstream github \
--upstream filesystemOr in .claude.json:
{
"mcpServers": {
"wombat": {
"command": "wombat",
"args": ["--manifest", "~/my-project/permissions.json", "--upstream", "github", "--dashboard", "7842"]
}
}
}Avoid relative paths for
--manifest. MCP servers are launched with an unpredictable working directory, so./permissions.jsonwill not resolve correctly. Use~/...or a full absolute path instead.
For OpenCode, add to opencode.json:
{
"mcp": {
"wombat": {
"type": "local",
"command": ["wombat", "--manifest", "./permissions.json", "--upstream", "github"],
"enabled": true
}
}
}Manage permissions from a dashboard instead of editing a local file. The gateway fetches a signed credential at startup and verifies it locally — no Wombat server is contacted during tool execution.
wombat --keygen
# [wombat] Gateway did:key : did:key:z6Mk...
# [wombat] Key file : ~/.config/wombat/gateway.keyCopy the did:key. You'll paste it into the dashboard when registering an agent.
Sign in at gateway.wombat.dev, register an agent with your did:key, define the permission grants, and click Sign & Issue Token.
The dashboard shows a ready-to-use CLI command:
wombat --token <agentId>.<token> --upstream githubThe --token value is two UUIDs joined by a dot: the agent's database ID and the opaque credential token. Copy the full string — the gateway needs both parts to authenticate the fetch request.
open http://localhost:7842Live view of every tool call: allowed (green) or denied (red).
The manifest defines what your agent is allowed to do. In local mode it's permissions.json. In cloud mode the dashboard generates it — same format, same enforcement.
{
"version": "0.1",
"umask": "----",
"grants": [
{ "resource": "github/my-org/my-repo", "mode": "rw--", "comment": "Full access to main repo" },
{ "resource": "github/my-org/my-repo/main", "mode": "r---", "comment": "Main is read-only" },
{ "resource": "filesystem/Users/yourname/Documents/project/**", "mode": "rw--", "comment": "Project files" },
{ "resource": "filesystem/Users/yourname/Documents/project/.env", "mode": "----", "comment": "Never touch .env" }
],
"sudo": { "enabled": false }
}Filesystem resources use the full absolute path (without the leading
/). Agents always pass absolute paths to filesystem tools, sofilesystem/projectwill not match — usefilesystem/Users/yourname/Documents/project/**instead.
Four characters: rwxd. Use - to deny that position.
| Mode | Meaning |
|---|---|
r--- |
read only |
rw-- |
read + write |
r-x- |
read + execute (e.g. trigger CI, no write) |
rwxd |
full access |
---- |
explicit deny |
More specific paths win. This means you can be permissive at the top and restrictive deeper:
github/org/repo/main ← wins (most specific)
github/org/repo ← second
github/org/* ← third
github/** ← least specific
Wombat ships with resolver tables for popular MCP servers:
wombat --upstream github # GitHub
wombat --upstream filesystem # Local filesystem
wombat --upstream slack # Slack
wombat --upstream postgres # PostgreSQL
wombat --upstream brave # Brave Search
wombat --upstream puppeteer # Browser automation
wombat --upstream memory # Knowledge graph
wombat --upstream fetch # HTTP requests
wombat --list # See all available serversInstall and Wombat auto-discovers them:
npm install @wombat-plugin/notion
npm install @wombat-plugin/linear
wombat --upstream notion --upstream linearFor internal servers, create wombat.plugins.js in your project root:
export default [{
name: "mycompany",
tools: {
mycompany__get_customer: ["r", (p) => `mycompany/customer/${p.id}`],
mycompany__update_customer: ["w", (p) => `mycompany/customer/${p.id}`],
}
}];No publishing required. Loaded automatically.
Wombat hashes permissions.json at startup. If the file is modified while running — including by the agent — all calls are denied until you restart.
# Store the manifest outside the agent's reach
wombat --manifest ~/wombat/permissions.json --upstream githubEven if the agent can't modify the manifest, it can still read it and use that knowledge to work around the rules. For Claude Code, add a permissions block to .claude.json to deny direct access:
{
"permissions": {
"deny": [
"Read(permissions.json)",
"Grep(permissions.json)"
]
}
}When using --token, the permissions are a BBS+ Verifiable Credential signed by your passkey in the browser. Wombat verifies the cryptographic proof independently at startup — no Wombat server is in the verification path.
- The credential cannot be forged. It is signed by your root
did:key, which never leaves your device. - The agent cannot edit its own permissions. Changing the credential requires forging a BBS+ proof — cryptographically infeasible.
- TTL bounds the blast radius. If a token is compromised, it expires at the stated time. Revoke early from the dashboard — the next gateway fetch returns 404.
- Store the gateway key outside the agent's reach. The default location (
~/.config/wombat/gateway.key) is outside any project directory.
Every call is logged to wombat-audit.jsonl in both modes:
{"ts":"2026-05-29T10:23:01Z","session":"sess_abc123","agent":"claude","tool":"filesystem__write_file","server":"filesystem","resource":"filesystem/Users/me/project/secret/keys.json","mode":"w","decision":"deny","ms":2,"manifest_hash":"abc...","matched_rule_resource":"filesystem/Users/me/project/secret/**","matched_rule_mode":"----"}Every denied entry includes the matched grant (resource + mode) or indicates the umask was used, so you can trace exactly which rule blocked the call and where to widen scope.
In dry-run mode, entries also include "dry_run":true.
Never logged: parameter values, file contents, API responses.
Wombat is an MCP proxy. Your agent talks to Wombat via stdio; Wombat forwards allowed calls to upstream servers.
Agent (Claude Code, OpenCode, etc.)
│
▼
┌────────────────────────────────────────────────┐
│ Wombat │
│ 1. Load manifest │
│ ├─ local: permissions.json (hash-locked) │
│ └─ cloud: BBS+ VC fetched via --token │
│ 2. Resolve tool call → resource │
│ 3. Check against manifest │
│ 4. Allow or deny │
└────────────────────────────────────────────────┘
│
├── allowed → upstream server
└── denied → error to agent
Every tool is annotated with its required mode ([read], [write], [exec], [delete]) in the tool list, so the agent can see the full permission map at startup. In dry-run mode (--dry-run), non-read calls are logged with a [DRY-RUN] marker and return an informative response instead of being forwarded — letting the agent discover what it would be able to do without side effects.
# Manifest source (exactly one required)
wombat --manifest <path> # Local permissions.json. Supports ~/ expansion.
wombat --token <token> # Cloud credential token from gateway.wombat.dev
# Format: <agentId>.<opaqueToken> — copy from the dashboard
# Cloud credential setup
wombat --keygen # Print this gateway's did:key and exit
wombat --key <path> # Gateway keypair path (default: ~/.config/wombat/gateway.key)
wombat --api-url <url> # API base URL (default: https://api.gateway.wombat.dev)
# Upstreams and tools
wombat --upstream <name> # MCP server to proxy (repeat for multiple)
wombat --list # List available servers
wombat --example # Show example permissions.json path
# Runtime
wombat --dashboard <port> # Dashboard port (default: 7842)
wombat --agent auto # Detect agent (claude-code or opencode)
wombat --audit <path> # Audit log path
wombat --live-reload # Dev mode: reload manifest on changes (incompatible with --token)
wombat --dry-run # Log non-read calls without forwarding them| Other tools | Wombat | |
|---|---|---|
| Scope | Allow/deny entire tools | r--- on main, rw-- on feature/* |
| Per-resource | No | Yes |
| Audit log | Sometimes | Always |
| Integrity check | No | Yes — file hash (local) or BBS+ proof (cloud) |
| Cloud-hosted policy | No | Yes — via --token |
No other tool lets you say: "Allow push_files to feature/*, deny it to main, and log every attempt."
MIT — LICENSE
Security issues: SECURITY.md