# Warden Cloak

**Prevention layer for secrets in AI coding agents.** Keeps real secret values out of the model's context, the on-disk JSONL transcript, and the upstream LLM API — while still letting the agent use those secrets to execute local tool calls.

Complements `warden sweep` (post-hoc disk-residue cleanup) by stopping leaks at the tool boundary in real time.

---

## How it works

You enroll a real secret once under a human-readable placeholder:

```bash
warden cloak add stripe_key    # value read from stdin, never argv
```

From that point on, the model refers to the secret as `@@SECRET:stripe_key@@`. When the agent emits a tool call containing that placeholder, Warden's `PreToolUse` hook:

1. Substitutes the placeholder with the real value in the command that is actually executed locally.
2. Wraps the command so its captured stdout/stderr passes through a `sed` filter that scrubs the real value *back* to the placeholder before Claude Code records it.

Result: the real secret is resident only inside the hook process and the local subprocess. The model, the JSONL transcript (`~/.claude/projects/<cwd>/*.jsonl`), and every upstream API request see only the placeholder.

```
            ┌──────────────────────────────── real value never here ──┐
            │                                                          │
User ──► Claude API ──► tool_use("@@SECRET:stripe_key@@")               │
                             │                                          │
                             ▼                                          │
                    PreToolUse hook                                     │
                    decloak + wrap                                      │
                             │                                          │
                             ▼                                          │
                 bash -c "( real cmd ) 2>&1 | sed s|REAL|@@...|g"       │
                             │                                          │
                             ▼                                          │
                  captured stdout (scrubbed) ──► tool_result ◄──────────┘
```

---

## Install

From inside a project:

```bash
warden cloak install
```

This merges four hook entries into `.claude/settings.json` (preserving any Warden runtime-monitor hooks already there):

| Event | Matcher | Script | Purpose |
|---|---|---|---|
| `PreToolUse` | `Bash\|Write\|Edit\|MultiEdit\|mcp__.*` | `secret-guard.sh` | Detect raw secrets in tool input, vault + deny |
| `PreToolUse` | `Bash` | `decloak.sh` | Substitute placeholders, wrap with `sed` |
| `PostToolUse` | `mcp__.*` | `recloak-mcp.sh` | Scrub real values from MCP responses |
| `UserPromptSubmit` | — | `userprompt-guard.sh` | Soft-block + auto-cloak pasted secrets |
| `Stop` (opt) | — | `sweep-on-stop.sh` | Dry-run sweep for residue (off by default) |

`secret-guard.sh` is registered *before* `decloak.sh` so it scans the model's
original input; it is also order-independent because it skips any value already
present in the vault (see below).

Flags:

```bash
warden cloak install --scope user           # install globally in ~/.claude
warden cloak install --no-userprompt-guard  # skip the paste-detection hook
warden cloak install --no-secret-guard      # skip the tool-call detect-and-block hook
warden cloak install --sweep-on-stop        # add the Stop-hook sweep
```

Uninstall leaves unrelated Claude Code settings untouched:

```bash
warden cloak uninstall
```

---

## Managing secrets

```bash
# Register — value is read from stdin (or hidden prompt if interactive)
warden cloak add stripe_key          # prompts you to paste the value
printf '%s' "$(cat ~/.keys/stripe)" | warden cloak add stripe_key
warden cloak add aws_prod --from-file ~/.keys/aws

# List registered placeholder names — NEVER shows values
warden cloak list

# Delete a registered secret (any tool call still referencing it will fail closed)
warden cloak remove stripe_key

# Show install state
warden cloak status
```

Secrets live under `$PRISMOR_HOME/secrets/` (default `~/.prismor/secrets/`) with the directory at `0700` and each file at `0600`. The directory should be **excluded from backups and sync** (Time Machine, iCloud, Dropbox). Override the location with `PRISMOR_SECRETS_DIR`.

---

## The user-prompt boundary

`UserPromptSubmit` hooks cannot *rewrite* a prompt in Claude Code — they can only block or add context. When you paste a prompt containing a recognizable secret (Stripe key, GitHub PAT, AWS access key, Slack token, GitLab PAT, JWT, …), the `userprompt-guard.sh` hook:

1. Detects the secret via conservative, known-prefix regex.
2. Auto-registers it under a deterministic hashed name (`auto_<8-hex>`) in `$PRISMOR_SECRETS_DIR`.
3. Blocks the submission with a message that shows the **sanitized** prompt, ready for you to copy and resubmit.

The UX cost is one re-paste per leaked prompt. The original prompt was *not* transmitted to the upstream API.

**Bypass:** prefix any prompt with `!!allow ` to skip detection for a single message (useful when you are deliberately discussing a secret format in prose).

---

## The tool-call boundary (detect-and-block)

`userprompt-guard.sh` only covers what *you* paste. But the model can introduce
a raw secret on its own — by generating one, reading it out of a file, or
copying a value from earlier output into a command, a file write, or an MCP
argument. `secret-guard.sh` (a `PreToolUse` hook) closes that gap.

When the model emits a tool call whose input matches a secret pattern, the hook:

1. **Vaults** the raw value under a deterministic `auto_<hash>` name — *unless
   that exact value is already in the vault* (so a value `decloak.sh` just
   substituted, or any manually-registered secret, is recognized as legitimate
   and passes through untouched).
2. **Denies** the call with a reason that names the `@@SECRET:auto_xxxx@@`
   placeholder to use instead — and never echoes the raw value.

The raw secret therefore never executes, is never written to a file, and never
reaches the transcript or the upstream API. The model sees the deny reason and
retries with the placeholder, which `decloak.sh` resolves for `Bash`. For
`Write`/`Edit`/MCP the placeholder (or an env-var reference) is what gets
stored — you should not be hardcoding live secrets into files anyway.

```
model emits raw  sk_live_…  in a Bash command
        │
        ▼
  secret-guard.sh  ── value already in vault?  ── yes ─► allow (no-op)
        │ no
        ▼
  vault as auto_<hash>  +  DENY("use @@SECRET:auto_<hash>@@ instead")
        │
        ▼
  model retries with the placeholder ──► decloak.sh substitutes & runs
```

Because of the vault check, install order between `secret-guard.sh` and
`decloak.sh` does not matter and retries are idempotent.

---

## Configurable detection patterns

Both guards detect secrets from a shared, file-based pattern set:

* **Built-in** — `builtin_patterns.txt` (shipped in this module): conservative,
  known-prefix credential formats (Stripe, GitHub, AWS, Google, Slack, GitLab,
  JWT). This is the single source of truth shared by the bash hooks and the
  Python CLI — no duplicated regex.
* **Custom** — `$PRISMOR_HOME/cloak_patterns.txt` (override with
  `$PRISMOR_CLOAK_PATTERNS`): your org-specific token formats.

Manage custom patterns — one POSIX ERE per line — with:

```bash
warden cloak pattern list                       # show built-in + custom patterns
warden cloak pattern add 'mycorp_[0-9a-f]{32}'  # validated, appended to custom file
warden cloak pattern remove 'mycorp_[0-9a-f]{32}'
```

`add` rejects an uncompilable regex; built-in patterns cannot be removed.

---

## What this does *not* protect

Enumerated honestly so you know what to layer on top:

1. **Hand-typed secrets shorter than ~16 chars.** No prefix, too short for entropy heuristics, indistinguishable from benign text.
2. **Secrets generated mid-turn** (`openssl rand`, `aws iam create-access-key`, `ssh-keygen`). The `sed`-wrap scrubber only knows about values already in `$PRISMOR_SECRETS_DIR`, so a freshly minted value flows through the *generating* command's output unscrubbed. `secret-guard.sh` does catch it on the *next* tool call if the value matches a known pattern (e.g. a generated `AKIA…` reused in a later command) — but a value with no recognizable shape still slips through.
3. **The secrets directory itself**, if committed, synced, or backed up without exclusion. Treat it as a single point of failure.
4. **Built-in `Read` of secret-bearing files.** `PostToolUse` can only rewrite MCP tool output, not built-in Read. Add a `permissions.deny` rule under `Read(./secrets/**)` in your Claude settings to close this gap, and route secret access through the placeholder syntax instead.
5. **Assistant-side narration.** If the model already saw a real value (through paste, Read, or a command that bypassed the wrapper), it can echo the value in prose. Hooks cannot filter assistant text. Use `/clear` immediately after any suspected leak.

For residue that slips through despite all of the above, run:

```bash
warden sweep            # dry-run scan
warden sweep --redact   # scan + encrypted-vault redact
```

---

## Layering with Warden runtime monitor

Cloak and Warden's runtime monitor coexist on the same `.claude/settings.json`. Install order does not matter; each feature merges its own matcher blocks and uninstall strips only its own entries. Recommended combination:

```bash
warden install-hooks --agent claude --mode enforce   # policy enforcement
warden cloak install                                 # secret prevention
```

Then on a cadence (or via the opt-in `--sweep-on-stop` flag above):

```bash
warden sweep --redact
```

---

## Files shipped in this module

```
warden/cloaking/
├── __init__.py             # public API re-exports
├── installer.py            # install/uninstall settings.json merger
├── secrets_store.py        # add/list/remove operations on $PRISMOR_SECRETS_DIR
├── patterns.py             # built-in + custom detection-pattern management
├── builtin_patterns.txt    # single source of truth for detection regexes
├── hooks/
│   ├── _patterns.sh        # shared bash loader (sourced by the guards)
│   ├── decloak.sh          # PreToolUse:Bash — placeholder substitution
│   ├── secret-guard.sh     # PreToolUse — detect raw secrets, vault + deny
│   ├── recloak-mcp.sh      # PostToolUse:mcp__.*
│   ├── userprompt-guard.sh # UserPromptSubmit soft-block
│   └── sweep-on-stop.sh    # Stop (opt-in)
└── README.md               # this file
```

Hook scripts are pure bash and depend only on `jq`. No Python startup cost on the hot path.
