Observed since 20 Oct, 21:55 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
agent-firewall1
A runtime security and governance layer that evaluates AI-agent tool requests before they execute.
A runtime security and governance layer that evaluates AI-agent tool requests
before they execute and returns one of three decisions: allow, ask
(require human approval), or block.
The engine is vendor-neutral and dependency-light (just pydantic + PyYAML)
so it can run inside a Claude Code PreToolUse hook with no network and no
database. The same engine can back an MCP gateway or an HTTP service.
It is a policy gate, not a sandbox. It never executes the tool it is judging, and it only reasons about what the integration hands it. Read
docs/limitations.mdfor the honest threat model before you rely on it.
Every request flows through a fixed, security-critical pipeline
(src/agent_firewall/engine.py):
authorization.py)detection/)risk/)allow / approval
/ deny, with the risk score available as a threshold. (policies/)deny > approval > allow. A high score can
only raise scrutiny; it can never rescue a denied request, and a low score
can never override a deny.Each decision is explainable (explanation, matched_policies,
matched_rules, risk_factors) and recorded to a tamper-evident audit log.
Requires Python ≥ 3.11.
pip install -e . # core engine + CLI
pip install -e ".[dev]" # + pytest, ruff, mypy, hypothesis
pip install -e ".[api]" # + optional FastAPI/SQLAlchemy service deps
pip install -e ".[mcp]" # + optional MCP gateway deps
The repo also works without installing: hooks/pre_tool_use.py inserts
src/ onto sys.path itself.
.claude/settings.json already wires the firewall as a PreToolUse hook over
the file, shell, and network tools:
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash|Read|Write|Edit|MultiEdit|NotebookEdit|WebFetch|Glob|Grep",
"hooks": [{
"type": "command",
"command": "python",
"args": ["${CLAUDE_PROJECT_DIR}/hooks/pre_tool_use.py"],
"timeout": 10
}]
}]
}
}
Decision mapping to the hook protocol:
| Engine decision | permissionDecision |
|---|---|
BLOCK | deny |
ASK | ask |
ALLOW | deferred to Claude Code's native flow (nothing emitted) unless AGENT_FIREWALL_EMIT_ALLOW=1 |
Deferring on allow means the firewall never disables native permission
controls — it only ever tightens them. Remove the hooks block (or the file) to
disable it entirely.
# Evaluate a synthetic request and print the full decision as JSON
agent-firewall evaluate --tool Bash --input '{"command": "rm -rf /"}'
# Validate a policy file against the schema
agent-firewall policy validate policies/default.yaml
# Inspect / verify the audit log
agent-firewall audit show --limit 20
agent-firewall audit verify
(agent-firewall hook runs the same stdin-driven hook adapter directly.)
The same engine can be exposed as a Model Context Protocol server so any MCP client can consult the firewall before running a tool:
pip install -e ".[mcp]"
agent-firewall mcp # serve over stdio
It registers one tool, evaluate_tool_request, that returns an
allow / ask / block decision for a described request. It is a consultative
gate — the calling agent must honor the decision. See
docs/mcp-gateway.md.
Policies are declarative YAML (policies/default.yaml). Selectors within a
policy are ANDed; lists within a selector are ORed. Effects are allow,
approval, or deny, with deny winning. The bundled set:
block-protected-credentials — never read/write .env, keys, .ssh, .aws, …block-critical-risk — deny anything scoring ≥ 85 (how rm -rf /, mkfs,
fork bombs, secret egress get hard-blocked — the score can't be argued away)require-approval-elevated-risk — score ≥ 50 (< 85) needs human approvalrequire-approval-network-egress — outbound tools (WebFetch) need approvalallow-project-reads — reading non-sensitive files is allowedRead from the environment (src/agent_firewall/config.py):
| Variable | Default | Meaning |
|---|---|---|
AGENT_FIREWALL_POLICY_FILE | bundled policies/default.yaml | policy document |
AGENT_FIREWALL_AUDIT_LOG | .agent_firewall/audit.jsonl | audit log path |
AGENT_FIREWALL_ON_ERROR | ask | fail mode on error: ask / deny / allow |
AGENT_FIREWALL_PROJECT_ROOT | cwd | project boundary for path checks |
AGENT_FIREWALL_ENV | development | environment label |
AGENT_FIREWALL_EMIT_ALLOW | unset | set 1 to emit explicit allow |
Append-only JSONL with a SHA-256 hash chain; audit verify detects truncation
or edits, and concurrent processes serialize appends via a lock file. Request
summaries are secret-scrubbed and raw tool input is never written verbatim. It
is tamper-evident, not tamper-proof — see
docs/limitations.md.
python -m pytest # test suite (unit tests in tests/)
python scripts/smoke_test.py # end-to-end sanity check on synthetic inputs
ruff check . # lint
mypy # strict type-check
All test fixtures and smoke-test inputs use synthetic credentials and safe, non-executed command strings.
src/agent_firewall/
engine.py core evaluation pipeline
authorization.py fail-closed identity/tool gate
normalize.py vendor payload -> NormalizedRequest
models.py typed domain models (pydantic)
config.py env-driven CoreConfig
detection/ command / filesystem / secrets / dlp / prompt-injection
risk/ deterministic additive scorer + weights
policies/ YAML schema, loader, evaluator
audit/ hash-chained JSONL writer + verifier
integrations/claude_code/ PreToolUse hook adapter
integrations/mcp/ MCP gateway server (optional [mcp] extra)
hooks/pre_tool_use.py thin stdin entry point for Claude Code
policies/default.yaml bundled policy set
docs/limitations.md threat model & known gaps
Contributions are welcome — see CONTRIBUTING.md for the dev
setup and the quality gate to run before a PR. To report a vulnerability, use
GitHub private vulnerability reporting as described in
SECURITY.md (please don't file public issues for security bugs).
MIT.
Download counts and popularity will appear here as the collector receives them.
| Package / target | Requirement | Relationship |
|---|---|---|
pydanticEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pydantic>=2.5 | runtime |
PyYAMLEnvironment{
"extras": "",
"marker": null,
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | PyYAML>=6.0 | runtime |
fastapiEnvironment{
"extras": "",
"marker": "extra == \"api\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | fastapi>=0.110; extra == "api" | runtime |
uvicornEnvironment{
"extras": "standard",
"marker": "extra == \"api\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | uvicorn[standard]>=0.27; extra == "api" | runtime |
SQLAlchemyEnvironment{
"extras": "asyncio",
"marker": "extra == \"api\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | SQLAlchemy[asyncio]>=2.0; extra == "api" | runtime |
aiosqliteEnvironment{
"extras": "",
"marker": "extra == \"api\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | aiosqlite>=0.19; extra == "api" | runtime |
mcpEnvironment{
"extras": "",
"marker": "extra == \"mcp\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | mcp<3,>=2.1; extra == "mcp" | runtime |
pytestEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest>=8.0; extra == "dev" | runtime |
pytest-covEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | pytest-cov>=4.1; extra == "dev" | runtime |
coverageEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | coverage>=7.3; extra == "dev" | runtime |
ruffEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | ruff>=0.4; extra == "dev" | runtime |
mypyEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | mypy>=1.8; extra == "dev" | runtime |
hypothesisEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | hypothesis>=6.90; extra == "dev" | runtime |
httpxEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | httpx>=0.27; extra == "dev" | runtime |
types-PyYAMLEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | types-PyYAML; extra == "dev" | runtime |
mcpEnvironment{
"extras": "",
"marker": "extra == \"dev\"",
"requires_python": ">=3.11",
"evaluation": "NOT_EVALUATED"
} | mcp<3,>=2.1; extra == "dev" | runtime |
bdist_wheel · 49.25 KB · Python py3
sdist · 40.51 KB · Python source
Observed since 20 Oct, 21:55 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package_version:0b67295af80e5434507bb31410cd8b893af49b34dd2fae2ed9a007beef01e231