Architecture
A local, file-based system with no network component.
All state lives in ~/.aiguard/.
System overview
The assistant's PreToolUse hook pipes each command, as JSON on stdin, to
aiguard-evaluate. The policy engine returns a Decision; the
exit code tells the assistant whether to run the command.
┌─────────────────┐ PreToolUse hook (JSON on stdin)
│ AI assistant │──────────────────────────────┐
│ (Kimi / Claude) │ ▼
└─────────────────┘ ┌───────────────────┐
▲ │ aiguard-evaluate │
│ exit 0: run └─────────┬─────────┘
│ exit 2: blocked, │ Decision
│ relay challenge token ▼
▼ ┌───────────────────┐
┌─────────────────┐ approve <token> │ policy engine │
│ user terminal │───────────────────▶│ allow / ask / │
│ aiguard approve│ │ block (+ risk) │
└─────────────────┘ └───────────────────┘
Touch ID (macOS) / TOTP (Linux) │
▼
~/.aiguard/: challenges/ approved/ audit.jsonl
The Decision contract
| Field | Values | Meaning |
|---|---|---|
action |
allow ask block | What happens to the command |
risk |
low medium high critical | Informational severity — never changes the action on its own |
warning | string | Plain-English explanation of what the command does |
Evaluation flow
- Prune expired challenges and approvals (opportunistic GC).
- Parse the hook payload (
tool_input.command) from stdin. - The policy engine returns a Decision.
allow→ audit, exit 0.block→ audit, exit 2 with the warning.ask→ if a matching approval exists for the exact command hash, consume it (single-use), audit, exit 0. Otherwise create a challenge file, audit, exit 2 withAIGUARD_CHALLENGE:<token>on stderr.- Any internal error → exit 2. AIGuard fails closed.
But the platforms fail open
Per Kimi Code CLI and Claude Code documentation, a hook that times out or crashes allows the command to run. AIGuard cannot be a hard gate — do not use it as your sole security barrier.
Policy engine: order of evaluation
First match wins:
- Absolute Block — recursive+force delete of protected
targets (
/,~,$HOME, …) and any write/delete/cp/mv/ln into~/.aiguard/. Cannot be overridden by anything. - User Policy — your
block,ask,allowregex patterns in~/.aiguard/policy.toml. - Built-in high-risk patterns —
curl|bash,sudo,dd of=/dev/,git push --force, … →ask. - Shell metacharacters (
;|&<>`$()and newlines) →ask. - Safe prefixes (
ls,pwd,git status, …) →allow. - Default →
ask.
An example user policy:
block = ["npm publish", "kubectl delete"] ask = ["^docker "] allow = ["^make test$"]
User policy can restrict, never unblock
Your rules can add restrictions and widen auto-allow, but no user rule — and no approval — can override an Absolute Block.
Challenge & approval flow
- A challenge token is
sha256(command)[:12]— deterministic per exact command string, so any edit to the command requires re-approval. - Approval authenticates you (Touch ID on macOS via PyObjC
LocalAuthentication; TOTP elsewhere or with
--totp), moves the challenge file fromchallenges/toapproved/, and stampsexpires_at = now + 300. - On retry, the approval file is consumed (single-use). Approvals expire after 5 minutes; unapproved challenges are pruned after 1 hour.
TOTP
RFC 6238, SHA-1, 6 digits, 30-second period, verification window
±1 step. Three failed verification attempts trigger a 5-minute lockout.
Secrets live in the system keyring where available, with a 0600-file
fallback (~/.aiguard/totp-secret) on headless systems.
State layout
| Path | Mode | Contents |
|---|---|---|
~/.aiguard/ | 0700 | All AIGuard state |
challenges/, approved/ | 0600 files | Pending challenges and single-use approvals |
audit.jsonl | 0600 | Append-only log of every command, verbatim |
policy.toml | — | Your user policy |
totp-secret | 0600 | TOTP fallback when no keyring is available |
Do not weaken these permissions
The 0700/0600 modes are part of the security model. The audit log may contain secrets passed as CLI arguments — handle it as sensitive.
Design records
Significant decisions are recorded as ADRs in the repository
(docs/adr/) — including PreToolUse hooks over native
instrumentation and out-of-band challenge-response. The
vocabulary used here (Decision, Action, Absolute
Block, Challenge, Approval, Fail-Open) is
defined in the repository's GLOSSARY.md.