ai-agent-guardrails
A small, tool-agnostic policy layer that sits between an autonomous AI coding
agent and the shell: it intercepts a proposed command, checks it against a
declarative threat model, and blocks the genuinely destructive ones —
terraform destroy with no reviewed plan, a namespace wipe, an unscoped
rm -rf, a force-push to main, an IAM policy that grants * on * — before
they execute, with every decision written to a structured audit log.
As coding agents (Claude Code, Cursor, and others) move from suggesting commands to actually running them, they need the same change-management discipline a human on-call engineer operates under: nothing destructive ships without a second set of eyes, and there's a record of what ran and why. Going into 2026, agent observability and guardrails have stopped being an afterthought bolted on after an incident — they're treated as a baseline requirement of AI-infrastructure design, on the same footing as auth or logging. This project is a small, concrete, working example of that layer: not a framework, not a platform, just the policy engine, an audit trail, a CLI, and one real integration (Claude Code), built to be read end-to-end in one sitting.
Threat model
Every rule lives in guardrails/policies/default.yaml as a
regex or a small Python check function (see
guardrails/checks.py for the ones that need more
than a regex — e.g. "was a plan reviewed earlier in this session?").
| Pattern | Why it's dangerous | Action |
|---|---|---|
terraform destroy with no -target and no evidence of a reviewed terraform plan -out= |
Can tear down an entire state file's worth of infrastructure with nobody having seen what would actually be deleted | BLOCK |
kubectl delete namespace <ns> |
Deletes every resource in the namespace — pods, services, secrets, PVCs, config — with no per-resource confirmation | BLOCK |
kubectl delete ... --all |
Removes every object of that kind in the target scope in one shot; equivalent blast radius to a namespace wipe | BLOCK |
rm -rf /, rm -rf ~, rm -rf ., or rm -rf with no path at all |
Recursive+force delete at a filesystem root/home/cwd, or with no path scoping, can destroy a machine irreversibly and near-instantly | BLOCK |
git push --force / -f to main/master/production |
Rewrites shared history other engineers and CI/CD depend on; can silently drop merged commits | BLOCK |
git push --force / -f to any other branch |
Still rewrites history, but the blast radius is smaller if the branch isn't shared | WARN |
IAM policy statement with "Resource": "*" and a broad action (iam:*, *:*) |
Grants effectively unrestricted access to the whole account — one of the most common causes of cloud account takeover | BLOCK |
| Disabling MFA / deleting an IAM user or access key | Removes an identity's ability to authenticate, or a control protecting it — accidental or malicious, this can lock out real operators or open a hole | BLOCK |
DROP TABLE / DROP DATABASE / TRUNCATE TABLE outside a clearly-named migration/rollback file |
Irreversible without a backup; expected and reviewable inside a migration, much easier to hit the wrong table when run ad hoc | WARN |
curl/wget piped directly into sh/bash |
Executes unreviewed, unpinned, potentially-mutable remote code with no chance to inspect it first — a common supply-chain vector | WARN |
Anything that doesn't match a rule is ALLOWed — this is a blocklist for known-dangerous patterns, not a default-deny sandbox.
Architecture
┌──────────────────────────────┐
proposed │ │ ALLOW ──▶ command runs
shell command ──▶│ PolicyEngine │
+ context │ (guardrails/policies/*.yaml │ WARN ──▶ command runs,
│ + guardrails/checks.py) │ reason logged
└──────────────┬────────────────┘
│ BLOCK ──▶ command refused,
▼ reason surfaced
┌─────────────────────┐
│ AuditLogger │ ──▶ ~/.guardrails/audit.jsonl
│ (JSON lines, one │ (tail into Loki/ELK/
│ record per call) │ Langfuse/etc.)
└─────────────────────┘
Two callers drive this today: the standalone guard CLI, and the Claude Code
PreToolUse hook. Both go through the same PolicyEngine.evaluate() — the
policy logic doesn't know or care which one called it, which is the point:
adding a Cursor, CI-bot, or LangGraph-tool integration means writing a thin
adapter that extracts a command string and calls evaluate(), not touching
the rules.
Quickstart
git clone <this-repo> ai-agent-guardrails
cd ai-agent-guardrails
python3 -m venv .venv && source .venv/bin/activate
pip install -e .
guard check "terraform destroy"
# [BLOCK] 'terraform destroy'
# rule: terraform_destroy_unreviewed
# reason: 'terraform destroy' with no -target and no evidence a plan was
# generated and reviewed first ...
# (exit code 1)
guard exec -- ls
# [ALLOW] 'ls'
# reason: No policy violation detected.
# <actual `ls` output follows>
guard exec -- rm -rf /
# [BLOCK] ... refusing to execute: command blocked by policy. (exit code 1, `rm` never runs)
Every call above appends a JSON record to ~/.guardrails/audit.jsonl.
Global flags (--json, --audit-log, --policy-dir, --context,
--no-audit) go before the subcommand, e.g. guard --json check "...".
Claude Code integration
integrations/claude_code/pretooluse_hook.py
implements Claude Code's PreToolUse hook contract: it reads the tool-call
JSON from stdin, pulls the Bash command out of it, runs it through
PolicyEngine.evaluate(), and on BLOCK prints a JSON decision to stdout plus
exits non-zero so Claude Code refuses to run the command. See the docstring
at the top of that file for the exact assumptions this makes about Claude
Code's hook I/O — the hook schema has changed across Claude Code releases,
and this is a best-effort reference implementation, not a guarantee it
matches whatever version you're running. Verify against your installed
version's hook docs before trusting it in anything that matters.
integrations/claude_code/CLAUDE.md.snippet
has the .claude/settings.json hook registration and a sample CLAUDE.md
section to paste into a project that wires this in.
This project is deliberately not Claude-Code-specific at its core — Claude
Code is one integration among several a policy engine like this should
support (Cursor, a CI approval bot, a custom agent loop) and the
guardrails/ package has no dependency on it.
Design decisions
Why a rule-based engine and not an LLM classifier? A BLOCK decision here is safety-critical: it has to be deterministic, explainable, and auditable — the same command should get the same decision every time, and the reason needs to be traceable to a specific rule, not a model's momentary judgment. An LLM classifier is a genuinely good complement for the fuzzy cases — "does this diff look like it's quietly widening an IAM policy," "does this commit message match what the diff actually does" — where the answer is a judgment call, not a fact. But mixing that into the hard BLOCK path means a prompt-injected or just-wrong model call could either wave through something that should never run, or refuse something completely benign for no reproducible reason. So: deterministic pattern-matching for the tier where being wrong is expensive (BLOCK), with room to layer a classifier on top for the WARN tier where a human is going to look at the output anyway. This mirrors how the rest of this author's tooling treats AI — as a risk-checker that flags things for a human, never as the thing making the final call.
Why regex + small Python checks instead of a full AST/shell parser? Most
of these patterns (branch names, path literals, JSON key/value pairs) are
easy to catch reliably with a regex or a couple of shlex.split() calls, and
keeping the rule format readable in YAML is worth more here than parsing
correctness for adversarial shell syntax. This is explicitly a reference
project, not a hardened sandbox — see "Status" below.
Status
This is a portfolio / reference project demonstrating the pattern, not a hardened production security boundary. In particular:
- Pattern matching can be evaded by determined obfuscation (
rm -rf $(echo /), base64-encoded commands, etc.) — it's built to catch the common, honest case of an agent about to run something destructive, not to resist an adversary deliberately trying to get around it. - It has one real integration (Claude Code). A Cursor or generic-CI adapter
would follow the same shape: extract a command string, call
PolicyEngine.evaluate(), act on theDecision. - The audit log is local JSONL by design (zero-dependency); wiring it into an actual Loki/ELK/Langfuse pipeline is a matter of tailing the file, not code changes here.
Extending the policy set
Add a rule to guardrails/policies/default.yaml:
- id: my_new_rule
action: WARN # or BLOCK
pattern: '(?i)some regex' # or: check: my_check_function
reason: >
Explain *why* this is dangerous, not just what matched.
For anything that needs more context than a regex over the raw command
string (session history, current branch, file path, etc.), add a
check(command: str, context: dict) -> bool function to
guardrails/checks.py and reference it by name via check: instead of
pattern:. Run pytest -q after any change — every rule above has both a
BLOCK/WARN test and at least one matching ALLOW test to guard against
regressions in either direction.
This is part of a small set of AI-infrastructure portfolio projects; sibling
repos (rag-mlops-pipeline, llm-observability-stack, ai-infra-terraform)
cover the rest of the stack — no links yet since they may not all be pushed.
Metadata
Release files for ai-agent-guardrails 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | |
|---|---|---|---|---|
| ai_agent_guardrails-0.1.1.tar.gz | 17.4 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ai_agent_guardrails-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 34.7 kB
Release files / ai_agent_guardrails-0.1.1.tar.gz
| Download URL | ai_agent_guardrails-0.1.1.tar.gz |
|---|---|
| Size | 17.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
51129876bec58bc06db380844c921fa952992638afc7d0eb4966cac27517a967
|
|
BLAKE2b-256 checksum How to use checksums |
a3d7547f1c34b6bba0297701adc03034b2d48012e32fc95fa2dff6c69dae1743
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Release files / ai_agent_guardrails-0.1.1-py3-none-any.whl
| Download URL | ai_agent_guardrails-0.1.1-py3-none-any.whl |
|---|---|
| Size | 17.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a053f6a194d993f38fc96c309b0e2248e9db3c99703c5baa14b86918ad3fe7f2
|
|
BLAKE2b-256 checksum How to use checksums |
8e04db32c8f8908ddaa963f39feb056e453020abcd94bf16d0f2b7f15a1f6ac0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.9.6
|
Security for ai-agent-guardrails 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.