The missing safety layer for AI agents.
Your agent can call tools. But should it?
agentgate gives you declarative permission rules, lifecycle hooks, and tool execution control for any AI agent — extracted from battle-tested patterns in production AI systems serving millions of users.
Your Agent (LangChain / CrewAI / OpenAI / Custom)
│
┌────▼─────────────────┐
│ agentgate │
│ permissions + hooks │
└────┬─────────────────┘
│
Actual Tools (filesystem, bash, APIs, databases)
Every AI agent framework gives you tools. None gives you governance.
| Feature | agentgate | LangChain | CrewAI | OpenAI Agents | ShipAny |
|---|---|---|---|---|---|
Permission DSL (allow/deny/ask) |
Yes | No | No | No | No |
| Lifecycle hooks (pre/post/error) | Yes | No | No | No | No |
| Provider-agnostic | Yes | Yes | Yes | No | No |
| MCP support | Yes | No | No | No | Yes |
| Type-safe tools (Zod) | Yes | Partial | No | Partial | Yes |
| Zero runtime deps | Yes | No | No | No | No |
npm install agentgate zodimport { agentgate, buildTool, matchPattern } from 'agentgate'
import { z } from 'zod'
const Bash = buildTool({
name: 'Bash',
inputSchema: z.object({ command: z.string() }),
call: async (input) => ({ data: `ran: ${input.command}` }),
preparePermissionMatcher: (input) => (pattern) => matchPattern(pattern, input.command),
})
const gate = agentgate({
permissions: {
mode: 'default',
rules: [
{ allow: 'Bash(git *)' }, // ✅ Allow git commands
{ deny: 'Bash(rm *)' }, // ❌ Block rm commands
],
},
tools: [Bash],
})
await gate.execute('Bash', { command: 'git status' }) // ✅ allowed
await gate.execute('Bash', { command: 'rm -rf /' }) // ❌ deniedDeclarative rules control what your agent can do. Rules use a compact DSL:
import { createPermissionEngine } from 'agentgate'
const permissions = createPermissionEngine({
mode: 'default', // 'strict' | 'default' | 'permissive' | 'bypass'
rules: [
{ allow: 'Read' }, // Allow entire tool
{ allow: 'Bash(git *)' }, // Allow by input pattern
{ deny: 'Bash(rm *)' }, // Deny by input pattern
{ ask: 'Write' }, // Require approval
{ allow: 'mcp__github__*' }, // Allow all MCP server tools
{ deny: 'mcp__filesystem__write_*' }, // Deny MCP write tools
],
})Permission modes:
strict— Deny by default. Only explicitly allowed tools run.default— Allow reads, ask for writes/destructive actions.permissive— Allow unless explicitly denied.bypass— Allow everything (testing only).
Hooks fire before/after tool execution for logging, validation, and control:
import { agentgate } from 'agentgate'
const gate = agentgate({
hooks: {
preToolUse: [{
matcher: 'Write', // Only for Write tool
hooks: [{
type: 'callback',
fn: async (input) => {
const { path } = input.toolInput as { path: string }
if (path.startsWith('/etc/')) {
return { continue: false, decision: 'block', reason: 'Cannot write to /etc/' }
}
return { continue: true, decision: 'approve' }
},
}],
}],
postToolUse: [{
hooks: [{
type: 'http',
url: 'https://audit.company.com/log', // Webhook for audit trail
}],
}],
},
})Hook types:
callback— In-process async function (fastest)command— Shell command (stdin: JSON input, exit code 0=approve, 2=block)http— HTTP POST webhook (JSON request/response)
Hook events: preToolUse, postToolUse, toolError, permissionDenied, sessionStart, sessionEnd, beforeCompact, afterCompact, configChange, custom
Build tools with Zod schemas and built-in safety classification:
import { buildTool } from 'agentgate'
import { z } from 'zod'
const DatabaseQuery = buildTool({
name: 'DatabaseQuery',
inputSchema: z.object({
query: z.string(),
database: z.enum(['production', 'staging', 'dev']),
}),
// Safety classification
isReadOnly: (input) => /^\s*SELECT\b/i.test(input.query),
isDestructive: (input) => /\b(DROP|TRUNCATE)\b/i.test(input.query),
// Tool-specific permission check
checkPermissions: async (input) => {
if (input.database === 'production' && !/^\s*SELECT\b/i.test(input.query)) {
return { behavior: 'deny', message: 'Only SELECT on production' }
}
return { behavior: 'allow', updatedInput: input }
},
call: async (input) => {
const result = await db.query(input.query)
return { data: result }
},
})The executor wires permissions + hooks + tools into a single lifecycle:
import { createToolExecutor, createPermissionEngine, createHookEngine } from 'agentgate'
const executor = createToolExecutor({
permissions: createPermissionEngine({ mode: 'default', rules: [...] }),
hooks: createHookEngine({ preToolUse: [...] }),
tools: [ReadTool, WriteTool, BashTool],
maxConcurrency: 10,
// Handle 'ask' permission decisions interactively
onPermissionRequest: async (toolName, message, input) => {
const approved = await promptUser(`Allow ${toolName}? ${message}`)
return approved
? { behavior: 'allow', updatedInput: input }
: { behavior: 'deny', message: 'User rejected' }
},
})
const result = await executor.execute('Bash', { command: 'git push' })
// result.status: 'allowed' | 'denied' | 'error' | 'blocked_by_hook'Connect to MCP servers and govern their tools with the same permission system:
import { createPermissionEngine } from 'agentgate'
import { createMCPClient } from 'agentgate/mcp'
const mcp = createMCPClient({
servers: {
filesystem: {
type: 'stdio',
command: 'npx',
args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'],
},
},
})
await mcp.connect()
const tools = mcp.discoverTools()
// tools: [{ name: 'mcp__filesystem__read_file', ... }, ...]
// Same permission rules work for MCP tools
const perms = createPermissionEngine({
rules: [
{ allow: 'mcp__filesystem__read_file' },
{ deny: 'mcp__filesystem__write_file' },
],
})| Export | Description |
|---|---|
agentgate(config) |
One-liner: creates a configured ToolExecutor |
buildTool(def) |
Create a type-safe tool with defaults |
createPermissionEngine(config) |
Create a permission engine |
createHookEngine(config) |
Create a hook engine |
createToolExecutor(config) |
Create an executor (permissions + hooks + tools) |
defineConfig(config) |
Type-safe config helper |
createMCPClient(config) |
Create an MCP client (from agentgate/mcp) |
"ToolName" → matches entire tool
"ToolName(pattern)" → matches tool + input pattern
"mcp__server__tool" → matches specific MCP tool
"mcp__server__*" → matches all tools from MCP server
"mcp__server" → matches all tools from MCP server
Patterns support wildcards:
"git *" → prefix match
"*.ts" → suffix match
"src/*.ts" → prefix + suffix
"*" → matches everything
agentgate is designed as middleware, not a framework. It sits between your agent and its tools:
- Agent requests tool call → agentgate receives
(toolName, input) - Permission engine evaluates rules →
allow,deny, orask - PreToolUse hooks fire → can approve, block, or modify input
- Tool executes with validated input
- PostToolUse hooks fire → logging, audit, side effects
- Result returns to agent
This works with any LLM provider and any agent framework because agentgate only cares about the tool layer, not the model layer.
- Runtime: Zero. No dependencies.
- Peer:
zod>= 3.23 (you likely already have it) - Optional peer:
@modelcontextprotocol/sdk>= 1.0 (only for MCP module)
See the examples/ directory — all runnable with npx tsx examples/<file>:
| # | Example | Use Case |
|---|---|---|
| 01 | Basic Permissions | Quick start with allow/deny/ask rules |
| 02 | Hooks and Lifecycle | Pre/post hooks for logging and validation |
| 03 | Custom Tools | Type-safe tools with Zod schemas |
| 04 | MCP Integration | Govern MCP server tools |
| 05 | Infrastructure Safety | Prevent agents from destroying your infra |
| 06 | Database Safety | Tiered DB access (prod read-only, staging read-write) |
| 07 | Audit Trail | SOC2/HIPAA/GDPR compliance logging with PII detection |
| 08 | Human-in-the-Loop | Interactive approval for risky actions |
| 09 | Input Validation | SQL injection blocking, PII redaction, email safety |
| 10 | Rate Limiting | Per-tool limits, budget caps, cost tracking |
Contributions welcome! Please open an issue first to discuss what you'd like to change.
git clone https://github.com/agentgate/agentgate
cd agentgate
npm install
npm test