Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 11 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,26 @@

Credential leak protection for [Claude Code](https://code.claude.com). Auth Guard keeps secrets out of your session transcripts with layered prevention, and finds the ones that got there anyway.

> [!WARNING]
> This is a **work in progress** based on my experience and observations with Claude Code.
> It is not yet (battle) tested and may not work as expected.
> A guard that fails silently can be worse than no guard, because it changes what you dare to do.
> Use at your own risk and verify the layers yourself (`/auth-guard:doctor` is a start, not a proof).
> Feedback is welcome. Please report any issues you encounter.

## What you get

**Two hooks**

- `secret-guard.sh` (PreToolUse, Bash): blocks or questions commands whose purpose is to print credentials (`git credential fill`, `gh auth token`, `security find-generic-password`, vault/password-manager reads, credential file access), with whitespace/quote normalization so trivial respellings do not slip past. Low-confidence matches `ask` instead of deny, so you stay in the loop.
- `output-guard.sh` (PostToolUse, Bash/Read/Grep/WebFetch): scans every tool output with [betterleaks](https://github.com/betterleaks/betterleaks) and rewrites it before the model sees it, replacing each finding with `[REDACTED:<rule-id>]`. This is the layer that catches the unanticipated route: secrets inside API responses, config dumps, logs, or files that no denylist modeled. Fails closed (output withheld) on scanner errors; fails open with a visible warning only when betterleaks is not installed at all.
- [`secret-guard.sh`](hooks/secret-guard.sh) (PreToolUse, Bash): blocks or questions commands whose purpose is to print credentials (`git credential fill`, `gh auth token`, `security find-generic-password`, vault/password-manager reads, credential file access), with whitespace/quote normalization so trivial respellings do not slip past. Low-confidence matches `ask` instead of deny, so you stay in the loop.

![A Claude Code session: the guard denies `gh auth token` outright, then asks for confirmation before `eval $(ssh-agent -s)`](docs/assets/deny-ask.gif)

- [`output-guard.sh`](hooks/output-guard.sh) (PostToolUse, Bash/Read/Grep/WebFetch): scans every tool output with [betterleaks](https://github.com/betterleaks/betterleaks) and rewrites it before the model sees it, replacing each finding with `[REDACTED:<rule-id>]`. This is the layer that catches the unanticipated route: secrets inside API responses, config dumps, logs, or files that no denylist modeled. Fails closed (output withheld) on scanner errors; fails open with a visible warning only when betterleaks is not installed at all.

![A Claude Code session: a local CLI prints a GitHub token, and the tool output reaches the model as `token=[REDACTED:github-pat]`](docs/assets/redact.gif)

**Three skills**

| Skill | What it does |
| --- | --- |
| `/auth-guard:doctor` | Verifies every layer: dependencies, hook presence and registration, sandbox and deny-rule settings, plus live self-tests of both hooks with a synthetic token. |
| `/auth-guard:global-settings` | Merges the bundled deny rules (`config/deny-rules.json`) into your `~/.claude/settings.json`. Additive and idempotent: dry-run first, timestamped backup, never removes or reorders your entries. |
| `/auth-guard:audit-transcripts` | Scans all stored session transcripts with betterleaks (regex fallback without it) and reports file paths and rule ids, never the matched text. |
| [`/auth-guard:doctor`](skills/doctor/SKILL.md) | Verifies every layer: dependencies, hook presence and registration, sandbox and deny-rule settings, plus live self-tests of both hooks with a synthetic token. |
| [`/auth-guard:global-settings`](skills/global-settings/SKILL.md) | Merges the bundled deny rules (`config/deny-rules.json`) into your `~/.claude/settings.json`. Additive and idempotent: dry-run first, timestamped backup, never removes or reorders your entries. |
| [`/auth-guard:audit-transcripts`](skills/audit-transcripts/SKILL.md) | Scans all stored session transcripts with betterleaks (regex fallback without it) and reports file paths and rule ids, never the matched text. |

**A deny-rule baseline** (`config/deny-rules.json`): `Read()`/`Bash()` permission denies plus `sandbox.credentials.files` entries for common credential stores (SSH, AWS, Azure, gcloud-style caches, gh, docker, kube, gnupg, browser profiles, shell history, and more). Some entries are opinionated; denying `~/.azure` breaks `az` inside sessions, for example. Delete what does not fit after applying.

Expand Down Expand Up @@ -77,7 +76,7 @@ mkdir -p ~/.config/auth-guard
$EDITOR ~/.config/auth-guard/custom-checks.json
```

Each check has a `name`, a `match` mode (`verb` or `any`), a `regex`, and a `decision` (`deny` or `ask`), plus an optional message and case-sensitivity flag. A deny always beats an ask, whichever side defined it, so a custom check can harden shipped coverage but never weaken it. Any error in the file fails closed and `/auth-guard:doctor` tells you which check is wrong.
A deny always beats an ask, whichever side defined it, so a custom check can harden shipped coverage but never weaken it. Any error in the file fails closed and `/auth-guard:doctor` tells you which check is wrong.

The schema, the resolution order, the `AUTH_GUARD_CUSTOM_CHECKS` override and the failure semantics are documented in **[docs/custom-checks.md](docs/custom-checks.md)**.

Expand Down
Binary file added docs/assets/deny-ask.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/redact.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 6 additions & 6 deletions docs/custom-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ before writing your first check. Any other top-level key is an error.
| `message` | no | string | The reason shown when the check fires. Defaults to `decision <decision> defined by custom-check <name>`. |
| `case_sensitive` | no | boolean | Defaults to `false`, matching the built-ins. Set `true` when case carries meaning, for example an environment variable name. |

Unknown keys, at either level, are an error rather than something ignored: a
misspelled key would otherwise silently drop the constraint you meant to write.
Unknown keys, at either level, are an error: a misspelled key would otherwise
silently drop the constraint you meant to write.

Regexes are passed to `grep` via `-e`, both when matching and when validating,
so a regex that begins with `-` is treated as data rather than as an option,
Expand Down Expand Up @@ -184,8 +184,8 @@ it after every edit to this file.

The file is read on every Bash call. It costs one `jq` invocation and a `grep`
per check, which is negligible against the hook overhead that is already there.
The guard makes no network calls. Beyond the `jq` and `grep` it already
required, it now also runs `base64`, which carries each validated field from
`jq` into the shell intact; it is part of POSIX and present on every system
that can run the rest of the plugin. If it is ever not working, the guard fails
The guard makes no network calls. Besides `jq` and `grep` it runs one more
tool, `base64`, which carries each validated field from `jq` into the shell
intact; it is part of POSIX and present on every system that can run the rest
of the plugin. If it is ever not working, the guard fails
closed rather than running with checks that silently decoded to nothing.