Skip to content
Closed
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,7 @@ rtk session # Show RTK adoption across recent sessions
```bash
-u, --ultra-compact # ASCII icons, inline format (extra token savings)
-v, --verbose # Increase verbosity (-v, -vv, -vvv)
--no-redact # Disable PII redaction for this invocation
```

## Examples
Expand Down Expand Up @@ -400,8 +401,16 @@ exclude_commands = ["curl", "playwright"] # skip rewrite for these
[tee]
enabled = true # save raw output on failure (default: true)
mode = "failures" # "failures", "always", or "never"

[redaction]
enabled = true # mask PII/secrets in ALL output, incl. proxy (default: true)
```

PII (emails, phone numbers, PAN, Aadhaar, card numbers, secrets) is redacted
as `[REDACTED:<category>]` before output reaches the LLM — see
[docs/redaction.md](docs/redaction.md) for categories, custom patterns,
allowlist and known gaps.

When a command fails, RTK saves the full unfiltered output so the LLM can read it without re-executing:

```
Expand Down
78 changes: 78 additions & 0 deletions docs/redaction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# PII Redaction

RTK redacts PII and secrets from **all** command output before it reaches the
LLM — including `rtk proxy` raw passthrough. Redaction is **ON by default**.

## What gets redacted

| Category | Examples | Replacement |
|----------|----------|-------------|
| `email` | `ravi@example.com` | `[REDACTED:email]` |
| `phone` | `+91 9876543210`, `+1 415 555 0132`, `(022) 4000 1234` | `[REDACTED:phone]` |
| `pan` | Indian PAN `ABCDE1234F` | `[REDACTED:pan]` |
| `aadhaar` | 12-digit numbers that pass the **Verhoeff** checksum | `[REDACTED:aadhaar]` |
| `card` | 13–19 digit numbers that pass the **Luhn** checksum (spaces/dashes allowed) | `[REDACTED:card]` |
| `secrets` | AWS access keys (`AKIA…`/`ASIA…`), JWTs, `Bearer` tokens, PEM private-key blocks, `api_key=`/`password=`/`secret=` assignments | `[REDACTED:aws_key]`, `[REDACTED:jwt]`, `[REDACTED:token]`, `[REDACTED:private_key]`, `[REDACTED:secret]` |
| `ip` | IPv4 addresses (loopback `127.*` and `0.0.0.0` stay visible) | `[REDACTED:ip]` |

Checksum gating (Luhn/Verhoeff) prevents false positives on commit SHAs,
epoch timestamps, UUIDs, job IDs and other long digit runs. The per-category
tag (rather than `****`) keeps output debuggable without leaking the value.

## Why this design is leak-proof

Redaction runs where raw process output is **first captured**
(`exec_capture`, `run_streaming`, `run_fallback`, pipe mode, and a
line-buffered copy loop in proxy mode) — not at print time. Filters, the
tee/recovery file, token tracking and the `never_worse` guard only ever see
already-redacted text, so no downstream "show raw instead" logic can
resurrect PII.

In proxy mode output is redacted per complete line, so a value split across
the 8 KiB pipe read boundary is still caught.

## Disabling / tuning

Per invocation:

```bash
rtk --no-redact proxy git log # raw output for this run only
```

Persistently, in `~/.config/rtk/config.toml`
(macOS: `~/Library/Application Support/rtk/config.toml`):

```toml
[redaction]
enabled = true # master switch (default: true)
email = true # per-category toggles (default: true)
phone = true
pan = true
aadhaar = true
card = true
secrets = true
ip = true

# lines matching these regexes are never redacted (e.g. test fixtures)
allowlist = ["EXAMPLE-DO-NOT-REDACT"]

# extra org-specific patterns
[[redaction.custom]]
name = "employee_id"
pattern = "EMP-\\d{6}"
```

Custom patterns are replaced with `[REDACTED:<name>]`. An invalid custom
regex is skipped with a warning — it never breaks the command.

## Known accepted gaps

- Commands with **no filter match** in the fallback path, and
`RunMode::Passthrough` commands, inherit stdio directly (interactive
tools, TTYs, `$EDITOR`) — RTK never sees their bytes, so it cannot redact
them. Same class of bypass as documented for pre-redaction `rtk proxy`.
- In proxy mode, a single line longer than **1 MiB** with no newline is
force-flushed in segments to bound memory; a value split exactly across
that forced flush point can be missed (pathological output only).
- The `allowlist` is line-scoped; multi-line PEM blocks are redacted before
the allowlist is applied.
37 changes: 37 additions & 0 deletions scripts/claude-srtk-hook.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash
# Claude Code PreToolUse hook: rewrite Bash commands to their srtk equivalent.
# srtk = rtk build with PII redaction (slice-ravichopra/rtk, feat/pii-redaction).
# Only rewrites the command text — permission checks still apply to the
# rewritten command as usual (no permissionDecision is emitted).
# Exits 0 with no output → command runs unchanged.
#
# Installed to ~/.claude/hooks/rtk-rewrite.sh by scripts/install-srtk.sh.
# Requires jq (brew install jq).

SRTK="$HOME/.local/bin/srtk"
[ -x "$SRTK" ] || exit 0

JQ="$(command -v jq)"
[ -n "$JQ" ] || exit 0

INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | "$JQ" -r '.tool_input.command // empty' 2>/dev/null)
[ -n "$CMD" ] || exit 0

# Never rewrite commands already using rtk/srtk.
case "$CMD" in
srtk\ *|rtk\ *|*"/srtk "*|*"/rtk "*) exit 0 ;;
esac

# srtk rewrite exit codes: 0 = rewritten (allow-listed), 3 = rewritten (no
# allow rule yet — Claude will ask as usual), 1 = no equivalent, 2 = deny.
REWRITTEN=$("$SRTK" rewrite "$CMD" 2>/dev/null)
rc=$?
{ [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; } || exit 0
[ -n "$REWRITTEN" ] || exit 0

# srtk emits "rtk ..." — force the srtk binary name.
REWRITTEN="srtk ${REWRITTEN#rtk }"

printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{"command":%s}}}' \
"$(printf '%s' "$REWRITTEN" | "$JQ" -Rs .)"
126 changes: 126 additions & 0 deletions scripts/install-srtk.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/bin/bash
# Install srtk (rtk with PII redaction) and wire it into Claude Code.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/slice-ravichopra/rtk/feat/pii-redaction/scripts/install-srtk.sh | bash
#
# What it does:
# 1. Downloads the srtk binary (Apple Silicon) to ~/.local/bin/srtk
# 2. Installs the Claude Code rewrite hook to ~/.claude/hooks/rtk-rewrite.sh
# 3. Registers the hook and renames rtk permission rules to srtk in
# ~/.claude/settings.json (backup written first)
# 4. Adds an "always use srtk" note to ~/.claude/RTK.md
#
# Safe to re-run (idempotent).

set -euo pipefail

REPO="slice-ravichopra/rtk"
TAG="${SRTK_TAG:-v0.42.4-pii.1}"
ASSET="srtk-darwin-arm64"
BIN="$HOME/.local/bin/srtk"
HOOK="$HOME/.claude/hooks/rtk-rewrite.sh"
SETTINGS="$HOME/.claude/settings.json"

if [ "$(uname -sm)" != "Darwin arm64" ]; then
echo "error: prebuilt binary is Apple Silicon only — build from source:" >&2
echo " git clone git@github.com:$REPO.git && cd rtk && cargo build --release" >&2
echo " cp target/release/rtk $BIN" >&2
exit 1
fi

command -v jq >/dev/null || { echo "installing jq..."; brew install jq; }

echo "==> downloading srtk $TAG"
mkdir -p "$(dirname "$BIN")"
curl -fsSL -o "$BIN" "https://github.com/$REPO/releases/download/$TAG/$ASSET"
chmod +x "$BIN"
"$BIN" --version

echo "==> smoke test"
OUT=$("$BIN" proxy sh -c 'echo probe a@b.co card 4111-1111-1111-1111')
echo "$OUT" | grep -q 'REDACTED:email' || { echo "error: redaction not working: $OUT" >&2; exit 1; }
echo " $OUT"

echo "==> installing Claude Code hook"
mkdir -p "$(dirname "$HOOK")"
# Embedded inline (kept in sync with scripts/claude-srtk-hook.sh) so a single
# fetch installs everything — no second download that could version-skew.
cat >"$HOOK" <<'HOOK_EOF'
#!/bin/bash
# Claude Code PreToolUse hook: rewrite Bash commands to their srtk equivalent.
# srtk = rtk build with PII redaction (slice-ravichopra/rtk, feat/pii-redaction).
# Only rewrites the command text — permission checks still apply to the
# rewritten command as usual (no permissionDecision is emitted).
# Exits 0 with no output → command runs unchanged.

SRTK="$HOME/.local/bin/srtk"
[ -x "$SRTK" ] || exit 0

JQ="$(command -v jq)"
[ -n "$JQ" ] || exit 0

INPUT=$(cat)
CMD=$(printf '%s' "$INPUT" | "$JQ" -r '.tool_input.command // empty' 2>/dev/null)
[ -n "$CMD" ] || exit 0

# Never rewrite commands already using rtk/srtk.
case "$CMD" in
srtk\ *|rtk\ *|*"/srtk "*|*"/rtk "*) exit 0 ;;
esac

# srtk rewrite exit codes: 0 = rewritten (allow-listed), 3 = rewritten (no
# allow rule yet — Claude will ask as usual), 1 = no equivalent, 2 = deny.
REWRITTEN=$("$SRTK" rewrite "$CMD" 2>/dev/null)
rc=$?
{ [ "$rc" -eq 0 ] || [ "$rc" -eq 3 ]; } || exit 0
[ -n "$REWRITTEN" ] || exit 0

# srtk emits "rtk ..." — force the srtk binary name.
REWRITTEN="srtk ${REWRITTEN#rtk }"

printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","updatedInput":{"command":%s}}}' \
"$(printf '%s' "$REWRITTEN" | "$JQ" -Rs .)"
HOOK_EOF
chmod +x "$HOOK"

if [ -f "$SETTINGS" ]; then
cp "$SETTINGS" "$SETTINGS.bak-srtk"
echo "==> updating $SETTINGS (backup: $SETTINGS.bak-srtk)"
# Rename rtk permission rules to srtk.
tmp=$(mktemp)
sed -e 's/Bash(rtk /Bash(srtk /g' -e 's/Bash(AWS_PROFILE=\* rtk /Bash(AWS_PROFILE=* srtk /g' \
"$SETTINGS" >"$tmp"
# Register the PreToolUse hook if not present.
jq --arg hook "$HOOK" '
.hooks.PreToolUse = ((.hooks.PreToolUse // []) as $p
| if ($p | map(select(.hooks[]?.command == $hook)) | length) > 0 then $p
else $p + [{"matcher":"Bash","hooks":[{"type":"command","command":$hook}]}] end)
' "$tmp" >"$SETTINGS"
rm -f "$tmp"
jq empty "$SETTINGS" || { echo "error: settings.json broken — restoring backup" >&2; cp "$SETTINGS.bak-srtk" "$SETTINGS"; exit 1; }
else
echo "==> creating $SETTINGS with hook registration"
mkdir -p "$(dirname "$SETTINGS")"
jq -n --arg hook "$HOOK" \
'{"hooks":{"PreToolUse":[{"matcher":"Bash","hooks":[{"type":"command","command":$hook}]}]}}' \
>"$SETTINGS"
fi

RTK_MD="$HOME/.claude/RTK.md"
if [ ! -f "$RTK_MD" ] || ! grep -q '## srtk — PII Redaction' "$RTK_MD"; then
echo "==> adding srtk note to $RTK_MD"
cat >>"$RTK_MD" <<'EOF'

## srtk — PII Redaction (IMPORTANT)

**Always use `srtk`, never plain `rtk`.** srtk masks PII (emails, phones,
PAN, Aadhaar, card numbers, AWS keys, IPs) as `[REDACTED:<category>]` before
output reaches the LLM — including `srtk proxy`. Disable per run with
`--no-redact` (only when the user asks). Details:
https://github.com/slice-ravichopra/rtk/blob/feat/pii-redaction/docs/redaction.md
EOF
fi

echo
echo "✅ srtk installed. Restart Claude Code sessions to pick up the hook."
1 change: 1 addition & 0 deletions src/cmds/system/pipe_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ pub fn run(filter_name: Option<&str>, passthrough: bool) -> Result<()> {
if buf.len() > RAW_CAP {
anyhow::bail!("stdin exceeds {} byte limit", RAW_CAP);
}
let buf = crate::core::redact::redact(&buf).into_owned();

let filter_fn = match filter_name {
Some(name) => resolve_filter(name).ok_or_else(|| {
Expand Down
Loading