termaxa 0.15.0

A cooperative gate for the shell commands AI coding agents run β€” command previews, automatic backups, allow/ask/deny policy, and audit logging.
<div align="center">

# πŸ›‘ Termaxa

**Run AI coding agents with confidence.**

Termaxa gates the shell commands an agent runs β€” previews the blast radius, backs up first, blocks the dangerous ones, and escalates repeat offenders. It's a cooperative windshield, not a sandbox.

[![CI](https://github.com/termaxa/termaxa/actions/workflows/ci.yml/badge.svg)](https://github.com/termaxa/termaxa/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/termaxa/termaxa?display_name=tag)](https://github.com/termaxa/termaxa/releases)
[![License](https://img.shields.io/badge/license-MIT%2FApache--2.0-blue)](#license)

</div>

---

Your AI agent wants to run `git push --force`, `DROP TABLE users`, `terraform apply`, `rm -rf`. Most of the time it's right. Sometimes it isn't. Today your only options are *supervise every command* (which defeats the point of an agent) or *trust it blindly* (which defeats your Friday).

Termaxa is a third option: a gate the agent's commands pass through. It reads a policy you wrote, shows you what's actually about to happen, backs up what's about to change, and records everything. Built for **Claude Code** and **Cursor** today; works as a standalone CLI anywhere.

```
  Claude Code --> TERMAXA --> git . postgres . docker . terraform . your shell
                    |
                    +- decide    allow / ask / deny  (your policy)
                    +- preview   commits lost, rows affected, resources destroyed
                    +- insure    automatic backup before destructive ops
                    +- escalate  repeated destructive intent -> auto-deny
                    +- record    every attempt, with an execution report
```

## Quick start (5 minutes)

**1. Install.** Download a prebuilt binary from [Releases](https://github.com/termaxa/termaxa/releases) and put it on your PATH β€” or, with a Rust toolchain:

```bash
cargo install termaxa
termaxa                       # what this is, and what to try next
termaxa check "rm -rf /"      # works immediately β€” no setup, no project config
```

**2. Wire up a project.**

```bash
cd your-project
termaxa init --claude-code      # writes .termaxa/policy.yaml, installs the Claude Code hook
termaxa doctor                  # confirm it's actually wired up
```

**3. See it work.**

```bash
termaxa check "git push --force origin main"
```

From now on, every Bash command Claude Code runs in this project passes through Termaxa first. Runtime state (logs, backups) lives in `~/.termaxa/`, safely **outside** your repo.

## What it looks like

### 1 - A destructive command can't hide behind a safe prefix

```console
$ termaxa check "git status && rm -rf /"
decision  deny
reason    segment 2/2 `rm -rf /` β€” Recursive delete from root is blocked.
```

Termaxa splits compound commands and judges each part. `git status &&` buys nothing.

### 2 - Blast radius, before you commit to it

```console
$ termaxa check "psql -d shop -c 'DROP TABLE users'"
decision  deny
reason    DROP TABLE is blocked. Archive or rename instead.

postgres impact
  DROP TABLE users
    rows (estimate) : 50,000
    referenced by   : audit_log, orders, sessions (3 tables)
    without CASCADE : this DROP will FAIL (dependents exist)
  insurance : pg_dump users before execution (automatic on run/hook)
```

Row estimates come from the planner (`pg_class.reltuples`, stale between `ANALYZE`s) β€” Termaxa never scans your tables.

### 3 - An agent that retries can't syntax its way through

An agent blocked on `rm -rf .` will often just try again with different words. Termaxa classifies the *intent*, not the spelling, and trips a per-session circuit breaker on repeat attempts:

```console
$ rm -rf .                        -> ask   (file-delete #1)
$ Remove-Item -Recurse -Force .   -> ask   (file-delete #2, different shell)
$ del /s /q .                     -> DENY  circuit breaker: 2 prior
                                     file-delete attempts this session
```

Three shells, one intent, third variant auto-denied β€” no rule enumerated per spelling. `find -exec rm`, `xargs rm`, and `unlink` count too. Configure via `circuit_breaker:` in `policy.yaml` (on by default, threshold 2).

### 4 - Destroy, then un-destroy

```console
$ termaxa run -- git push --force origin main
β”Œ push preview (main -> origin)
β”‚  ⚠ remote will LOSE 1 commit(s):
β”‚    βœ— 44510f1 important work
β””
Proceed? [y/N] y
πŸ›Ÿ backup b-1783006590625 β€” origin/main @ 44510f1 pinned to termaxa/backup/b-1783006590625
$ termaxa rollback b-1783006590625
βœ“ origin/main restored to 44510f1
```

Force push measures what the remote will *lose*, not just gain β€” and pins it to a backup branch first.

### 5 - What a delete actually costs

Deletes are the most common destructive command and the easiest to get wrong,
because a path can look correctly scoped right up until it isn't:

```console
$ termaxa check "rm -rf /c/Users/harih"
decision  ask
reason    no rule matched; policy default is `ask`

delete impact
  target      : C:\Users\harih
  as written  : /c/Users/harih
  ⚠ OUTSIDE the project root (C:\Users\harih\project)
  ⚠ resolves to a USER PROFILE directory
  ⚠ contains  : .ssh (SSH private keys), .aws (AWS credentials)
  contains    : 5,000+ files (stopped counting) across 422 directories
  βœ— insurance : too large to copy (5,000+ files) β€” NOT recoverable
```

`/c/Users/harih` is Git Bash syntax for `C:\Users\harih` β€” a real user
profile, not a stray directory. Termaxa resolves the path, counts what's
actually inside it (budgeted: 5,000 files or 300ms, and it says when it
stopped counting), flags credentials in the blast radius, and tells you
whether a backup is even possible.

An ordinary in-project delete says none of that, which is the point β€” a
warning that fires on `rm -rf ./target` is a warning nobody reads:

```console
$ termaxa check "rm -rf ./target"
delete impact
  target      : /home/you/project/target
  contains    : 1,204 files across 38 directories
  insurance   : copy 1 path(s) to .termaxa/backups before deletion
```

**What it can't do:** know what you meant. If the path has a typo in it,
Termaxa will faithfully report the blast radius of the path you actually
typed. Making that gap visible before execution is the whole contribution.

### Is it actually wired up?

The failure mode nobody warns you about: the hook is installed, the agent doesn't call it, and everything looks fine. `termaxa doctor` answers the question directly.

```console
$ termaxa doctor

Termaxa doctor
──────────────────────────────────────────
βœ“ termaxa 0.15.0
  /home/you/.cargo/bin/termaxa

Policy
βœ“ /home/you/project/.termaxa/policy.yaml
  67 rule(s), default ask
  fingerprint 1aa53b6e0d64
  βœ“ unchanged since 2026-08-13T18:10:22Z

Agents
βœ“ Claude Code  hook configured and live

Preview support
βœ“ git        force-push previews and git backups
Β· psql       Postgres blast radius unavailable
Β· pg_dump    Postgres backups unavailable
Β· terraform  plan previews unavailable

State
βœ“ /home/you/.termaxa/projects/project-4005e00d
  3 audit entries (3 from hooks)

──────────────────────────────────────────
βœ“ Everything checks out.
  proof is in the log: run your agent, then `termaxa report`
```

**Configured and live** is earned, not assumed: doctor invokes the registered hook command exactly as the agent would β€” synthetic must-deny payload on stdin, two-second timeout β€” and requires a decision back. Three states: **configured and live** (it answered), **registered but NOT firing** (a registration exists, the command doesn't run β€” worse than absent, because it's the state that *looks* safe), and **not configured**. Until v0.15 doctor only checked that a registration existed; a hook whose path was mangled at exec failed non-blocking, two full sessions ran ungated, and doctor said "configured" in green throughout.

Two honest boundaries. The probe only runs binaries named `termaxa` β€” a settings file arrives with a cloned repo and is untrusted input. And **live means "answered when doctor invoked it"**: if the agent's own invocation is broken on the agent's side, the probe can't see that β€” which is why doctor pairs it with the log. Live here plus no recent hook entries there means the agent has never reached the gate; doctor says so and points you at `TERMAXA_HOOK_DEBUG`, because agents rename their hook APIs, and when they do, the gate fails open and silent (see [Honest limitations](#honest-limitations)). Doctor is read-only, probe included: no backup, no audit entry, no notification β€” proven by test against the real binary.

### After a session: the report

```console
$ termaxa report

Session   session a3f8c21
──────────────────────────────────────────
Duration            18 min
Commands            41   βœ“ 34 Β· ? 6 Β· βœ— 1
Escalated           2
Auto-flow           34
Previews            4
Backups             3
Rollbacks           0

Destructive intents
──────────────────────────────────────────
file-delete         5
db-destroy          1
breaker trips       1

Insight
──────────────────────────────────────────
The breaker blocked file-delete 1 time in this scope.

This often indicates:
β€’ generated files being cleaned
β€’ build/output directories
β€’ an agent retry loop

If this work is intentional, add an explicit allow rule
scoped to the paths involved β€” relaxation is deliberate.

Recent events
──────────────────────────────────────────
? git push --force origin main
βœ— psql -d shop -c "DROP TABLE users"
βœ“ cargo test

Backups   : 3 β€” rollback available (`termaxa backups`)
Risk      : High    (denyΓ—3 + escalationΓ—2 + askΓ—1 = 13)

Last 30 days
──────────────────────────────────────────
Sessions        12
Commands        341
Decisions       βœ“ 302 Β· ? 31 Β· βœ— 8
Backups         19
Breaker trips   3

Top directories
  api
  crates/core
  web
```

One command, no flags: what the agent tried, what got blocked, what's recoverable β€” plus a 30-day view. Note that *destructive intents* and *breaker trips* are separate numbers: a legitimate `rm -rf ./build` is a classified intent, not a trip.

Every line is a fact with a source in the audit log. Nothing invented, nothing collected: the report reads the local append-only log, makes no network calls, and sends no telemetry.

## Why Termaxa?

**"Claude Code already asks permission β€” why do I need this?"**

The built-in prompt tells you the *command*. Termaxa tells you the *consequence*: 50,000 rows, 3 dependent tables, 1 commit lost. It takes the backup **before** you approve, and when it blocks something it tells the model *why*, so the agent proposes an alternative instead of retrying.

**Why not a sandbox / Docker / Claude Code's `/sandbox`?**

A sandbox contains damage *to the sandbox*. But your repo, your database, and your Terraform state are exactly the real things an agent must touch to be useful β€” and a sandbox's default write scope *is* your working directory. Containment, consequence, and recovery are three different questions: sandboxes answer the first, Termaxa answers the second and third. They're complementary β€” run both. ([Longer version.](https://termaxa.com/blog/claude-code-sandbox))

**Why not OPA / policy engines?**

OPA decides allow/deny well. It has no execution previews, no automatic backups, no rollback, and no agent-native hook. Termaxa is policy *plus* the things you actually want when an agent is holding the keyboard.

## Architecture

```
                     a command the agent wants to run
                                  |
        +-------------------------v-------------------------+
        |                       TERMAXA                       |
        |                                                   |
        |  shell split -> policy -> context -> decision     |
        |  (&&, ;, |)     (yaml)   (branch,    (allow/      |
        |                          flags,       ask/deny)   |
        |                          prod, SQL)      |        |
        |                                          v        |
        |              preview <-------------- consequential|
        |         (git loss, pg blast radius,      |        |
        |          terraform plan)                 v        |
        |              insurance <------------- destructive |
        |         (git ref / pg_dump / files)      |        |
        |                                          v        |
        |                                       execute     |
        |                                          |        |
        |  audit (JSONL, ~/.termaxa) <---------------+        |
        |  notify (webhook)   report (session summary)      |
        +---------------------------------------------------+
```

Six engines, one binary. Policy is in-repo (`.termaxa/policy.yaml`, reviewable in PRs); logs and backups live in `~/.termaxa/` where no `git` operation can touch them.

## Policy

`.termaxa/policy.yaml` β€” first match wins, `*` is a wildcard, matching is case- and whitespace-insensitive:

```yaml
version: 1
default: ask                     # unmatched commands require approval

rules:
  - match: "git status*"
    action: allow
  - match: "git push*--force*"
    action: ask
    reason: "Force push β€” remote history will be overwritten."
  - match: "*drop table*"
    action: deny
    reason: "DROP TABLE is blocked. Archive or rename instead."

circuit_breaker:                 # optional (on by default)
  enabled: true
  threshold: 2                   # trip on the 3rd repeated destructive attempt

notify:                          # optional
  webhook: https://hooks.slack.com/services/...
  on: [deny, ask]
```

## Command reference

| Command | Purpose |
|---|---|
| `termaxa` | what this is, and what to try next |
| `termaxa init [--claude-code]` | scaffold `.termaxa/`, detect tools, install the hook |
| `termaxa doctor` | is the gate wired up? binary, policy, agents, tools, state |
| `termaxa check "<cmd>"` | dry-run: verdict + preview (exit 0/3/4) |
| `termaxa run -- <cmd>` | gated execution: preview β†’ approve β†’ backup β†’ run |
| `termaxa hook` | agent hook mode (stdin JSON β†’ decision) |
| `termaxa log [--decision D] [--source S] [--json]` | the audit trail |
| `termaxa stats` | totals, sessions, top blocked |
| `termaxa backups` Β· `termaxa rollback <id>` | list / restore backups |
| `termaxa report [--session ID] [--all] [--days N] [--md]` | session summary + rollup |
| `termaxa notify --test` | verify your webhook |
| `termaxa paths` | where policy and state live |

Colour is on when output is a terminal and off when it isn't. `NO_COLOR`, `TERMAXA_NO_COLOR`, and `CLICOLOR_FORCE` are all respected.

## Honest limitations

Termaxa is pre-1.0. It's real and tested, and it is not magic. Specifically:

- **Hooks advise; they don't enforce.** Termaxa gates commands an agent submits through the Claude Code or Cursor hook. Those agents are *cooperative* β€” they respect a `deny` and propose an alternative, which is what makes the gate work. An agent running in full-auto mode could, in principle, retry a blocked action through a different command or shell; the circuit breaker raises the cost of that, but a hook is an *integration* point for visibility and policy, not an *enforcement* boundary. True enforcement means owning the execution path β€” that's **Termaxa Runtime**, on the roadmap. For hard guarantees today, pair Termaxa with OS-level sandboxing.
- **Native agent tools bypass the gate.** The hook sees *shell* commands. An agent's own built-in file/edit tools don't go through the shell β€” observed in live testing, a Cursor agent switched to its native file-delete tool and removed files Termaxa never saw. Non-shell tool calls need OS-level isolation underneath.
- **Cooperative, not a sandbox.** Termaxa governs commands that flow through the agent hook or `termaxa run`. An agent with raw, unhooked shell access is *not* contained β€” that needs OS-level sandboxing, a complementary layer. The threat model is *agents making expensive mistakes*, not a malicious agent actively evading you.
- **Shell parsing is good, not perfect.** It splits on `&&`, `||`, `;`, `|`
  and flags `$(...)`. Subshells `( )`, deeply nested quoting, and
  variable-expanded commands are judged conservatively, not deeply
  understood. A path built from a variable (`rm -rf ~/x/$SID`) is evaluated
  as written: the delete preview will report the blast radius of
  `~/x/$SID` literally, and if the variable expands to empty at runtime the
  real target is the parent. Termaxa cannot see the caller's environment.
- **Previews are best-effort.** No database connection β†’ static analysis only. Terraform previews shell out to `terraform plan`. Remote Terraform state is versioned by its backend, not by Termaxa.
- **Backups have edges.** `rm` insurance keys on the literal `rm` command. Postgres backups use `pg_dump`/`psql` and must be on your PATH. No retention/pruning yet β€” backups accumulate.
- **The format may still change.** Pre-1.0 means the policy schema and CLI can shift between minor versions. Pin a release.
- **Claude Code and Cursor are live-tested.** Both are exercised end-to-end, including the circuit breaker tripping under a real Cursor session. Codex and Copilot dialects parse each agent's format but aren't verified end-to-end yet. Help validating them is welcome.
- **Windows PowerShell 5.1 mangles redirected Unicode.** `termaxa report > out.txt` writes UTF-16 and garbles the box-drawing glyphs. That's the shell, not Termaxa β€” use PowerShell 7, or `termaxa report --md | Out-File -Encoding utf8 report.md`.

See [SECURITY.md](SECURITY.md) for the full threat model.

## Contributing

Issues and PRs welcome. `cargo test` must pass; CI runs on Linux, macOS, and Windows. The codebase is ~10,700 lines of dependency-light Rust β€” `src/policy.rs` and `src/preview.rs` are the best places to start reading.

If you can make an agent get past the gate in a way that isn't already documented above, that's the most useful contribution you can make: [open an issue](https://github.com/termaxa/termaxa/issues) or email security@termaxa.com.

## License

Dual-licensed under either [MIT](LICENSE-MIT) or [Apache 2.0](LICENSE-APACHE), at your option.
Contributions are accepted under the same terms β€” dual MIT/Apache-2.0, at the
user's option. No CLA.