Guardrails for Claude Code when it works on AWS and Terraform. Fifteen mods that stop risky commands before they run, stop commands that destroy work for good, lock outbound commands once a session has read untrusted content, catch secrets before they are written, flag insecure Terraform as it is edited, keep long command output out of the model's context, keep subagents and Codex on the models you choose, and show how much of your Claude and Codex plans is left.
At a glance
- Problem: an AI coding agent works at the speed of its tool calls. One
terraform apply -auto-approve, one deleted IAM role or one access key pasted into a file is enough to do damage, and a long plan or scanner run fills the context the agent needs to reason about it. - Approach: hooks that sit on the agent's tool calls. Guards deny the call and tell the model what to do instead, before anything runs. Linters add a note the model reads right after the edit. Tools hand the model a short summary in place of thousands of lines of raw output.
- Result: 1010 tests across the fifteen mods, run in CI against a pinned Claude Code release. Each guard's tests pin both what it denies and the ordinary commands it must leave alone.
These are guardrails against an agent's mistakes, not a security boundary. They read command lines and file text; a determined person, or a script that builds a command at run time, can get past them. Keep IAM permissions, SCPs and branch protection as the real controls. The known limits of each mod are listed below.
| Mod | Kind | Works in | What it does |
|---|---|---|---|
| terraform-guard | Guard | CLI and Desktop | Denies terraform apply/destroy with -auto-approve, and apply without a saved plan file. Covers tofu and terragrunt. |
| aws-guard | Guard | CLI and Desktop | Denies AWS CLI and AWS Tools for PowerShell calls that delete IAM roles or policies, change Organizations, or destroy KMS keys, and hands the command to the user to run. |
| destructive-guard | Guard | CLI and Desktop | Denies what destroys work for good: a recursive delete outside the project or a temp folder, git push --force (not --force-with-lease), reset --hard, clean -f, checkout ., stash clear, DROP/TRUNCATE/DELETE without WHERE sent to a database client, and disk formats. |
| self-guard | Guard | CLI and Desktop | Denies changes to what Claude Code loads as its own config or code: ~/.claude (except memory, transcripts and plans), ~/.claude.json, every folder in CLAUDE_CODE_PLUGIN_DIRS, and a project's .claude/settings*.json, whether by file tool, shell or claude plugin/mcp/config. Reads still run. |
| taint-guard | Guard | CLI and Desktop | Once the session reads untrusted content (a web page, an MCP result, a curl), denies what could send data out or publish: network tools, git push, gh and AWS writes, terraform apply, package publishes, untrusted MCP tools, a WebFetch whose URL carries data (a query string, userinfo, or an encoded path segment or host label). A plain gh api GET to GitHub stays usable. You lift the lock with /taint-clear, and the session continues where it stopped. A WebSearch only warns: its results are marked untrusted, but it does not lock. |
| tf-lint | Guard and linter | CLI and Desktop | Denies edits that write an AWS access key, a private key or an API token. Flags Terraform that hardcodes the arn:aws: partition, opens SSH/RDP to 0.0.0.0/0, creates a KMS key without rotation or a 7-30 day deletion window, or sets a GovCloud provider without FIPS endpoints. |
| commit-identity-guard | Guard | CLI and Desktop | In the GitHub owners you name, denies commits and PRs carrying a Claude co-author or "Generated with Claude Code" trailer, and commits whose author or committer is not a GitHub noreply address. |
| model-guard | Guard | CLI and Desktop | Starts every subagent on the model you name, and denies Codex runs that leave out the flags you require, such as --model and --effort. |
| jev-guard | Guard | CLI and Desktop | Denies Jev (TypeSafe) calls that would send a secrets, tfvars or state file, a key or token, a secret-named variable, or GovCloud/CUI work (by region, profile or folder name) to the TypeSafe API. |
| secret-allow-guard | Reminder | CLI and Desktop | At session start, warns when a permission rule in the project's or your own .claude/settings*.json holds a literal secret ("Yes, and don't ask again" saves the whole command: PGPASSWORD=, sshpass -p, mysql -p, a bearer token, a user:pass@ URL, an AWS, GitHub, OpenAI, npm, Slack or Supabase key, a private key), and when the project's settings.local.json is tracked by git or not git-ignored. It names the file and the count, never the value. |
| tf-plan-summary | Tool | CLI and Desktop | A plan_summary tool: a saved plan as counts, every destroy and replace, and IAM/KMS/security group/S3 policy changes, by attribute name only. |
| output-trimmer | Context | CLI and Desktop | Cuts long output of noisy commands (terraform, scanners, installs) to its head, error lines and tail, after saving the whole output to a file. |
| gh-ci-status | Display and tool | CLI and Desktop | A band with the checks of your open pull requests, and Dependabot's and your workflows' (release-please) on your repositories, what holds each merge (needs approval, changes requested, a deployment protection rule holding a job, a branch rule, workflow runs waiting for approval), Merge/Rebase/Fix/Approve buttons, and a checks tool that returns the tail of a failing Actions log. |
| aws-context | Display | CLI and Desktop | The AWS profile, region and partition above the prompt, highlighted for GovCloud and for the profiles you name. |
| cleanup-check | Reminder | CLI and Desktop | A status line after each turn (in Desktop, a line above the prompt): a merged branch still checked out, merged branches to delete, saved plan files left behind, and stray keys, certificates, .env files and Terraform state not in git. |
| usage-band | Display and reminder | Desktop; the Codex warning in both | Your Claude plan's and Codex plan's 5-hour and weekly usage left above the prompt, each with when it resets: green with more than half left, orange at half or less, red at 20% or less, and LOW under 15%. Codex is there for sessions that hand work to it, such as reviews, second opinions and parallel tasks run through the Codex plugin, which spend the Codex plan rather than Claude's. Claude's figures are the ones the session reports; Codex's are read from its session logs, and show once Codex has run in the last seven days. After a Codex run with a window at 20% left or less, it tells the model so the next review can wait, and notifies you once per window. |
Works in is where each mod does its job. A mod that only hooks tool
calls, prompts and turns, or adds a tool, works the same in the CLI and in
Claude Code Desktop. On screen the two differ: both draw the band above the
prompt and the tool-call rows, but Desktop does not draw a mod's status
line under the prompt, so cleanup-check shows its line above the prompt
there instead. usage-band draws its band in Desktop alone, since in the CLI
the same figures belong in your own statusLine command.
Guards deny rather than ask: in auto mode an "ask" is settled by a classifier, not by a person, so a deny is the only answer that reliably reaches you.
Requires Claude Code 2.1.309 or later (the version CI tests against) and
git. gh-ci-status needs the GitHub CLI signed
in; tf-plan-summary needs terraform.
-
Clone the repository:
git clone https://github.com/DustyStudy/claude-code-guardrails.git
-
List the mods you want in
CLAUDE_CODE_PLUGIN_DIRSin theenvblock of~/.claude/settings.json, separated by;on Windows and:elsewhere:{ "env": { "CLAUDE_CODE_PLUGIN_DIRS": "/path/to/claude-code-guardrails/mods/terraform-guard:/path/to/claude-code-guardrails/mods/aws-guard:/path/to/claude-code-guardrails/mods/tf-lint" } }Point it at a checkout that stays on
main, not the one you develop in: mods load from disk, so switching branches there changes the guards every session runs. A worktree does it:git worktree add ../claude-code-guardrails-live main, thengit -C ../claude-code-guardrails-live pullafter each merge. -
Start a new Claude Code session. The mods load at startup.
Four mods read a setting from the same env block. Each is optional.
commit-identity-guard, aws-context and model-guard do nothing extra until
theirs is set.
| Variable | Mod | Value |
|---|---|---|
COMMIT_GUARD_OWNERS |
commit-identity-guard | Comma-separated GitHub owners whose repos the rules cover, such as my-user,my-org. |
AWS_CONTEXT_HIGHLIGHT |
aws-context | Comma-separated text that marks a sensitive profile name, such as security,111122223333. |
MODEL_GUARD_SUBAGENT_MODEL |
model-guard | The model every subagent starts on, such as opus. A fork always runs on the session's own model. |
MODEL_GUARD_CODEX_ARGS |
model-guard | The flags every Codex run must pass, written as you pass them, such as --model gpt-6-astra --effort high. |
TAINT_GUARD_TRUSTED |
taint-guard | Comma-separated MCP tool name prefixes whose results are trusted and which stay usable in a tainted session, such as mcp__plugin_context7_,mcp__plugin_semgrep_guardian__. tf-plan-summary's tool is always trusted; gh-ci-status's is not, since an Actions log carries a pull request's text. |
TAINT_GUARD_TRUSTED_HOSTS |
taint-guard | Comma-separated hosts whose pages a WebFetch reads without tainting the session, such as docs.aws.amazon.com,learn.microsoft.com. A host also covers its subdomains. Name the bare domain, not a www. host: WebFetch follows a redirect between the two, so a www. host alone is never trusted. Name only sites whose pages no outsider writes: not github.com or a forum. curl still taints; WebSearch only warns. |
TAINT_GUARD_READ_SERVERS |
taint-guard | Comma-separated MCP tool name prefixes, one per server, whose read tools (named search, get, list, read or query) stay usable in a tainted session. Their results still taint it, and the server's other tools are still denied. A read's arguments reach that server, so name only servers no outsider runs: first-party connectors such as Gmail, and the desktop app's own mcp__ccd_session_mgmt__,mcp__ccd_settings__. |
TAINT_GUARD_BROWSER_HOSTS |
taint-guard | Comma-separated hosts, matched exactly (no subdomains), where the browser tools (Claude in Chrome and the desktop app's built-in browser) work without tainting the session, such as create.roblox.com,www.roblox.com,roblox.com. The guard reads each tab's address from the browser's own tab list, fresh before and after the call, never from page text: a call stays clean when its tab is on a listed host both times and any page it opens is listed (a blank new tab counts; a tab list call needs every tab listed). Once tainted, one step on such a tab still runs, and opening and closing tabs always do; page scripts (javascript_tool) do not. Any other site, an unknown tab, a call with no tab ID and no single active tab, or a browser_batch with more than one page step taints, or once tainted is denied, as before. |
usage-band reads $CODEX_HOME, or ~/.codex when that is not set. The
usage appears once the Codex CLI, signed
in with a ChatGPT plan, has run on this machine in the last seven days.
npm ci # installs the pinned Claude Code release
npm test # runs every mod's tests, and checks the shared parser copies matchEach mod's tests run with claude plugin test mods/<name>. To type-check a
mod, open it once in a Claude Code session so the engine writes its
.claude-plugin/types folder (ignored by git), then run
npx -p typescript tsc -p mods/<name>.
terraform-guard, aws-guard, destructive-guard, taint-guard, model-guard and self-guard each carry the same
hooks/shell.ts, which finds where a command line really runs a program
(through bash -c, env, xargs, subshells, substitutions and PowerShell
script blocks). Mods load folder by folder, so each keeps its own copy; CI
fails if the copies drift apart.
Each limit is also marked in the code with a shortcut: comment that says
when it would be worth lifting.
- Command-line guards (terraform-guard, aws-guard, commit-identity-guard) read the command as written. They do not expand shell variables or aliases, and a script file that runs the command itself is not read.
- aws-guard covers the AWS CLI and AWS Tools for PowerShell cmdlets called by name. SDK scripts, and a cmdlet called through a variable, are not checked.
- tf-lint reads only the text being written, block by block. A rule split across edits or built from variables is missed; run Checkov on the whole file for that.
- commit-identity-guard reads a message file given with
-F,--fileor--body-file, but not a message piped in with-F -. It identifies a repo by itsoriginremote. - secret-allow-guard checks at session start, so a rule approved during
a session is caught by the next one. It matches known key formats and
secrets typed after a flag or variable name, so a bare password with no
marker next to it is missed; a value written as
$VARpasses. Ignore the file in every repo on the machine, including ones cloned later, withgit config --global core.excludesfile ~/.gitignore_globaland.claude/settings.local.jsonas a line in that file. - aws-context takes a profile's region from the command, then the
environment, then the profile's section of
~/.aws/config(orAWS_CONFIG_FILE), which it rereads at most every 30 seconds. It does not followsource_profileor SSO session settings. - output-trimmer empties saved logs older than seven days each time it
saves one. The mod API has no file delete, so the empty files stay in
$TEMP/claude-outputuntil you delete that folder. - usage-band knows Codex's usage as of Codex's last report, so it updates when Codex runs, not as time passes; a window whose reset time has passed reads full. It reads Codex's session log format, which Codex may change, and skips a log over 4 MiB. A Codex run is recognized by its command line.
- model-guard checks Codex runs made through the Codex plugin's
companion script or the
codexCLI by name, and reads the flags as written:-m,-modeland--model=all count as--modeland the last one wins, and a flag set only in Codex's config does not count. The companion'sreviewandadversarial-reviewtake no--effort, so they need only the other flags and run at the effort in Codex's config. The CLI has no--efforteither: it takes-c model_reasoning_effort=x, and-c model=x,-mxand-m=xcount as the model, every one of which must match; a profile,--ossor another provider is denied. - destructive-guard reads a delete's targets as written: a path built
at run time,
find -delete, a script file, or SQL in a file given with-fis not read. The project is the session's folder unless that is home or a drive root, where only temp folders count. Acdin the command adds its folder as a base for relative targets, in any order. Paths are compared as written, so a symlink or junction inside the project that points out of it is followed byrmbut not by the guard. Cloud deletes (aws s3 rb,terminate-instances) are left to aws-guard and IAM. - taint-guard cuts the blast radius of a prompt injection; it does not
detect one. It knows the sinks by program name, so a script that opens a
socket itself (
python -c,node -e) passes, as does a file read with Read from a repo someone else wrote. A tainted WebFetch without a query string still runs, so data in a URL path or subdomain gets out; deny WebFetch or use/sandboxwith a network allowlist for that. WebSearch stays usable and does not lock: its query goes to the search provider, not to a host the attacker reads, but its snippets can still carry an injection, which then meets no lock. It reads command lines with the shared parser, so a quoted argument with spaces under a wrapper (env git -C "a b" push) is split and can hide a subcommand. az and gcloud are denied whole. The lock lasts for the session; a new session starts clean. - cleanup-check does not fetch, so a branch merged on GitHub shows once
something runs
git fetch --prune. - gh-ci-status lists the 20 most recently updated open pull requests of
yours and 20 of Dependabot's (on repositories you own, not an org's). A
failing log over 4 MiB is cut by the host before the tail is taken; the
checkstool says so when it happens.
See SECURITY.md to report a way past a guard.