CLAUDE.md · git:20261018.5f098bd · 2026-10-18 · sha256 76623c843b87d757
CLAUDE.md git:20261018.5f098bdA
Immutable. This exact content never changes and is served at /api/v1/blob/76623c843b87d757.
# claude-aws-guard A Claude Code plugin (`aws-guard`) containing a PreToolUse hook that guards `aws` CLI calls made through the Bash tool. Written in TypeScript, developed and tested with Bun, shipped as a built file that runs on Bun or Node. ## Layout | Path | Purpose | |---|---| | `plugins/aws-guard/src/aws_guard.ts` | Entry point. Reads the tool call as JSON on fd 0, prints one decision, exits. Owns all I/O: stdin, the audit line on stderr, the fail-closed `catch`. | | `plugins/aws-guard/src/aws_guard_lib.ts` | Pure policy logic (`shellSplit`, `parseAws`, `loadConfig`, `evaluate`). No I/O, so it is unit tested in-process. | | `plugins/aws-guard/dist/aws_guard.mjs` | The built file the plugin actually runs. **Committed.** Rebuild after any change under `src/`. | | `plugins/aws-guard/hooks/hooks.json` | Hook registration and the shell wrapper command. | | `plugins/aws-guard/.claude-plugin/plugin.json` | Plugin manifest, including the `userConfig` options. | | `.claude-plugin/marketplace.json` | Marketplace listing for the plugin. | | `examples/settings.json` | Manual-install settings plus the `permissions.deny` second layer (a plugin can't ship those). | | `test/aws_guard_lib.test.ts` | In-process tests. Owns the full policy matrix, the parser and the config. | | `test/aws_guard.test.ts` | Subprocess tests: a small smoke set, config through env vars, stdin handling, the built file under Node, fail-closed, audit log, the `hooks.json` wrapper. | | `scripts/bench.ts` | Latency benchmark. Results in `docs/benchmarking.md`. | | `docs/hardening.md` | How to stop Claude changing the hook. Not applied by default. | | `SECURITY.md`, `CHANGELOG.md`, `README.md` | Public docs. | This `CLAUDE.md` is deliberately at the repo root, outside `plugins/aws-guard/`: a `CLAUDE.md` inside a plugin isn't loaded as context and `claude plugin validate` warns about it. ## Commands ```sh bun install bun run typecheck # tsc --strict bun test # all tests bun test --coverage # coverage covers the lib only bun run build # rebuild dist/ (use Bun 1.4.2: CI pins it) bun run check-dist # fail if dist/ differs from a fresh build bun run validate # claude plugin validate, plugin and marketplace bun run bench # latency ``` Coverage only sees the lib, because `aws_guard.ts` runs in a subprocess in the tests. Keep the lib at 100% function and line coverage. ## Rules to preserve - **Fail closed.** Any error inside the script becomes a deny. The `hooks.json` wrapper turns a crash into exit 2 and sets `"onFailure": "block"`, so timeouts and other hook failures block too. Don't weaken either. - **Non-`aws` commands pass through** with no output, exit 0 and no audit line. There is no `passthrough` value for `permissionDecision`; silence is how it is expressed. - **The audit log is written by the shell**, not the script. The script writes one JSON line per `aws` call to stderr (`ts`, `decision`, `reason`, `cmd`), and the wrapper appends stderr to `~/.claude/aws-audit.log`. If that file can't be opened, the wrapper blocks the call. - **Configuration fails closed too.** Anything in `loadConfig` that can't be parsed is an error, never a default. A config error denies `aws` calls only and must never affect other commands. The built-in hard denies and the explicit-profile requirement cannot be configured away. - **Keep `aws_guard_lib.ts` free of I/O**, and keep `aws_guard.ts` thin. - **Keep startup cheap.** The hook runs before every Bash call. Don't read stdin through `process.stdin` (about 17 ms extra on Bun) and don't add imports with heavy startup. Re-run `bun run bench` if you change how the script starts. - **Runtime preference is Bun, then Node.** Bun starts about four times faster. The hook must keep working on Node, and the tests run the built file under Node. - **Decision order in `evaluate()` matters:** configuration error, then compound/indirect commands ask, then hard denies (built-in, then configured), then the blocked-profile pattern, then missing profile, then the read-verb `--query` rules, then `s3 ls` and `--dry-run`, then ask for everything else. ## When changing a rule 1. Change `plugins/aws-guard/src/aws_guard_lib.ts`. 2. Add or update the row in the policy table in `test/aws_guard_lib.test.ts`. 3. Update `permissions.deny` in `examples/settings.json` and in the README if it is a hard deny, since they duplicate the script's denies on purpose. 4. Only touch the smoke set in `test/aws_guard.test.ts` if a decision type or the I/O path changed. 5. `bun run build`, then `bun run typecheck && bun test --coverage && bun run validate`. 6. Update `README.md` and `CHANGELOG.md` if behavior or options changed. ## Known limits See "What it doesn't protect against" in the README and `SECURITY.md`. The ones that matter when editing: - The hook only sees the Bash command string. - Anything mentioning `aws` after whitespace, a quote or a `/` is treated as an `aws` call, so unrelated compound commands that merely contain the word get an `ask` and an audit line. That is intentional. - The wrapper's `2>>` redirect and `||` assume a POSIX shell. - Verified against the hooks docs: exit 2 blocks, other non-zero exits don't (hence `onFailure`), `permissionDecision` is `allow`/`deny`/`ask`/`defer`, `timeout` is in seconds. Observed working inside Claude Code: deny (reason shown to Claude), allow and hook-failure blocking. **Not yet observed:** how an `ask` prompts an interactive user, and whether plugin hooks honor `onFailure` exactly like settings hooks (`claude plugin validate` accepts it). - `onFailure` needs Claude Code 2.1.295 or later.