Check for PII being sent by your AI models A developer released ai-log-grep.sh, a bash script that searches local AI assistant chat logs and session transcripts for a sensitive string such as an email address, surfacing where an agent may have echoed it into commands, headers, or outbound requests. The tool covers logs from Claude Code, Claude Desktop, OpenAI Codex CLI, ChatGPT Desktop, Gemini CLI, Cursor, VS Code Copilot Chat, Windsurf, Continue, Zed, LM Studio, and Open WebUI, with a --deep flag that also scans the home directory for aider histories. It notes that web-only chats live server-side and cannot be scanned from disk, and that SQLite stores are searched via strings, so matches indicate presence in the database rather than a parsed message. | | /usr/bin/env bash | | | | | | ai-log-grep.sh — search local AI assistant chat logs / session transcripts | | | for a sensitive string e.g. your email to find where an agent may have | | | echoed it into commands, headers, or outbound requests. | | | | | | Usage: | | | ./ai-log-grep.sh "you@example.com" | | | ./ai-log-grep.sh --context "you@example.com" print text around each match | | | ./ai-log-grep.sh --deep "you@example.com" also scan $HOME for aider histories slow | | | ./ai-log-grep.sh --all "you@example.com" list every matching file default: top 10 per folder | | | | | | Covers where present on macOS/Linux : | | | Claude Code, Claude Desktop, OpenAI Codex CLI, ChatGPT Desktop, | | | Gemini CLI, Cursor, VS Code Copilot Chat , Windsurf, Continue, | | | Zed, LM Studio, Open WebUI default sqlite path , aider --deep | | | | | | Notes: | | | - Only local logs. Web-only chats claude.ai, chatgpt.com, gemini.google.com | | | live server-side and can't be scanned from disk. | | | - SQLite stores Cursor, VS Code, Open WebUI are scanned via strings , | | | so matches there mean "present in the DB", not a parsed message. | | | - Matches that are only git metadata commit authors, Co-authored-by | | | trailers, git config user.email , ... and anything under .git/ are ignored. | | | - Set NO COLOR=1 to disable colors. | | | set -uo pipefail | | | DEEP=0 | | | CONTEXT=0 | | | SHOW ALL=0 | | | while $ -gt 0 ; do | | | case "$1" in | | | --deep DEEP=1; shift ;; | | | --context CONTEXT=1; shift ;; | | | --all SHOW ALL=1; shift ;; | | | -- shift; break ;; | | | break ;; | | | esac | | | done | | | NEEDLE="${1:?usage: $ basename "$0" --deep --context --all \"string-to-find\"}" | | | case "$ uname -s " in | | | Darwin APPDATA="$HOME/Library/Application Support" ;; | | | APPDATA="${XDG CONFIG HOME:-$HOME/.config}" ;; | | | esac | | | --- Terminal styling --------------------------------------------------------- | | | TTY=0; -t 1 && TTY=1 | | | COLOR=0; $TTY -eq 1 && -z "${NO COLOR:-}" && COLOR=1 | | | if $COLOR -eq 1 ; then | | | B=$'\033 1m' DIM=$'\033 2m' RS=$'\033 0m' | | | RED=$'\033 31m' GRN=$'\033 32m' YEL=$'\033 33m' MAG=$'\033 35m' CYN=$'\033 36m' | | | else | | | B='' DIM='' RS='' RED='' GRN='' YEL='' MAG='' CYN='' | | | fi | | | FRAMES= ⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏ | | | WORK="$ mktemp -d "${TMPDIR:-/tmp}/ai-log-grep.XXXXXX" " \|\| exit 1 | | | WORKER PID= | | | cleanup { | | | -n "$WORKER PID" && kill "$WORKER PID" 2 /dev/null | | | $TTY -eq 1 && printf '\033 ?25h' | | | rm -rf "$WORK" | | | } | | | trap cleanup EXIT | | | trap 'exit 130' INT TERM | | | --- Matching ----------------------------------------------------------------- | | | Reads one file or stdin . If NEEDLE occurs outside of git metadata, prints | | | the occurrence count, then with --context up to 3 one-line excerpts. | | | find matches { | | | local perl program=' | | | my $n = $ENV{NEEDLE}; my $p = 0; my $count = 0; my $last end = -1; my @ex; | | | my $hl, $rs = $ENV{COLOR} ? "\e 1;33m", "\e 0m" : "", "" ; | | | while $p = index $ , $n, $p = 0 { | | | my $s = $p 150 ? $p - 150 : 0; | | | my $pre = substr $ , $s, $p - $s ; | | | my $match = $p; | | | $p += length $n; | | | next if $pre =~ / ?:Author\|Commit\|Committer\|Co-authored-by\|Signed-off-by\|Tagger\|Reviewed-by\|Acked-by\|Helped-by\|Reported-by \s : ^< {0,100}<\z/i; | | | next if $pre =~ / ?:author\|committer -mail <\z/; | | | next if $pre =~ /\b ?:author\|committer ?email\\?"?\s := \s \\?"?\z/i; | | | next if $pre =~ /user\.email\W{0,5}\z/; | | | $count++; | | | next unless $ENV{CONTEXT} && @ex < 3 && $match = $last end; | | | my $start = $match 80 ? $match - 80 : 0; | | | my $end = $match + length $n + 80; | | | $end = length $ if $end length $ ; | | | $last end = $end; | | | my $c = substr $ , $start, $end - $start ; | | | $c =~ s/\\ nrt / /g; | | | $c =~ s/ \x00-\x1f\x7f +/ /g; | | | $c =~ s/ {2,}/ /g; | | | $c =~ s/\Q$n\E/$hl$n$rs/g; | | | push @ex, $start 0 ? "…" : "" . $c . $end < length $ ? "…" : "" ; | | | } | | | END { if $count { print "$count\n"; print "$ \n" for @ex; exit 0 } exit 1 } | | | ' | | | if $ -gt 0 ; then | | | NEEDLE="$NEEDLE" CONTEXT="$CONTEXT" COLOR="$COLOR" perl -0777 -ne "$perl program" -- "$1" | | | else | | | NEEDLE="$NEEDLE" CONTEXT="$CONTEXT" COLOR="$COLOR" perl -0777 -ne "$perl program" | | | fi | | | } | | | --- Source groups -------------------------------------------------------------- | | | Each group: label, kind text\|sqlite\|deep , newline-separated paths. | | | G LABEL= G KIND= G PATHS= G STATE= | | | add group { kind, label, path... | | | local kind="$1" label="$2"; shift 2 | | | local paths="" state=missing p | | | for p in "$@"; do | | | paths+="$p"$'\n' | | | if $kind == sqlite ; then -f "$p" && state=waiting | | | else -e "$p" && state=waiting | | | fi | | | done | | | G LABEL+= "$label" G KIND+= "$kind" G PATHS+= "$paths" G STATE+= "$state" | | | } | | | add group text "Claude Code" \ | | | "$HOME/.claude/projects" "$HOME/.claude/history.jsonl" "$HOME/.claude/todos" | | | add group text "Claude Desktop" \ | | | "$APPDATA/Claude/logs" "$APPDATA/Claude/Session Storage" | | | add group text "Codex CLI" \ | | | "$HOME/.codex/sessions" "$HOME/.codex/history.jsonl" "$HOME/.codex/log" | | | add group text "ChatGPT Desktop" \ | | | "$APPDATA/com.openai.chat" "$APPDATA/ChatGPT" | | | add group text "Gemini CLI" \ | | | "$HOME/.gemini/tmp" "$HOME/.gemini/history" | | | add group text "Cursor chats " \ | | | "$APPDATA/Cursor/User/workspaceStorage" "$APPDATA/Cursor/User/globalStorage" | | | add group sqlite "Cursor state DBs " \ | | | "$APPDATA/Cursor/User/workspaceStorage"/ /state.vscdb \ | | | "$APPDATA/Cursor/User/globalStorage/state.vscdb" | | | add group text "VS Code / Copilot Chat" \ | | | "$APPDATA/Code/User/workspaceStorage" | | | add group sqlite "VS Code state DBs " \ | | | "$APPDATA/Code/User/workspaceStorage"/ /state.vscdb \ | | | "$APPDATA/Code/User/globalStorage/state.vscdb" | | | add group text "Windsurf" \ | | | "$HOME/.codeium/windsurf" "$APPDATA/Windsurf/User/workspaceStorage" | | | add group text "Continue" \ | | | "$HOME/.continue/sessions" | | | add group text "Zed AI threads " \ | | | "$HOME/.local/share/zed/threads" "$APPDATA/Zed/threads" | | | add group text "LM Studio" \ | | | "$HOME/.lmstudio/conversations" "$HOME/.cache/lm-studio/conversations" | | | add group sqlite "Open WebUI" \ | | | "$HOME/open-webui/webui.db" "$HOME/.open-webui/webui.db" | | | $DEEP -eq 1 && add group deep "Deep scan aider histories " "$HOME" | | | NG=${ G LABEL @ } | | | --- Scanning runs in a background worker per group --------------------------- | | | record match { results-file, path, find matches-output | | | local res="$1" path="$2" out="$3" | | | local ctx="$res.ctx.$ wc -l < "$res" \| tr -d ' ' " | | | printf '%s\n' "$out" \| tail -n +2 "$ctx" | | | printf '%s\t%s\t%s\n' "${out%%$'\n' }" "$path" "$ctx" "$res" | | | } | | | scan group { index | | | local i=$1 res="$WORK/res.$1" p f out | | | : "$res" | | | while IFS= read -r p; do | | | -n "$p" \|\| continue | | | case "${G KIND i }" in | | | text | | | -e "$p" \|\| continue | | | while IFS= read -r f; do | | | out="$ find matches "$f" " && record match "$res" "$f" "$out" | | | done < < grep -rIls --exclude-dir='.git' --exclude=' .vscdb' --exclude=' .db' --exclude=' .sqlite' -- "$NEEDLE" "$p" 2 /dev/null | | | ;; | | | sqlite | | | -f "$p" \|\| continue | | | out="$ strings -- "$p" 2 /dev/null \| find matches " && record match "$res" "$p" "$out" | | | ;; | | | deep | | | while IFS= read -r f; do | | | out="$ find matches "$f" " && record match "$res" "$f" "$out" | | | done < < find "$p" \ | | | $ -path "$HOME/Library" -o -path "$HOME/.Trash" -o -name node modules -o -name .git $ -prune -o \ | | | -type f $ -name ".aider.chat.history.md" -o -name ".aider.input.history" $ -print 2 /dev/null | | | ;; | | | esac | | | done <<< "${G PATHS i }" | | | } | | | files in { -f "$WORK/res.$1" && wc -l < "$WORK/res.$1" \| tr -d ' ' \|\| echo 0; } | | | matches in { -f "$WORK/res.$1" && awk -F'\t' '{s+=$1} END {print s+0}' "$WORK/res.$1" \|\| echo 0; } | | | plural { $1 -eq 1 && printf "%s %s" "$1" "$2" \|\| printf "%s %s" "$1" "${3:-${2}s}"; } | | | --- Live dashboard ---------------------------------------------------------------- | | | TICK=0 | | | DRAWN=0 | | | draw row { | | | local i=$1 icon status color | | | case "${G STATE i }" in | | | missing color=$DIM icon='–' status="not found" ;; | | | waiting color=$DIM icon='·' status="waiting" ;; | | | running color=$CYN icon="${FRAMES TICK % ${ FRAMES @ } }" | | | status="scanning… $ plural "$ files in "$i" " file so far" ;; | | | done | | | local nf; nf=$ files in "$i" | | | if $nf -gt 0 ; then | | | color=$YEL icon='●' status="$ plural "$nf" file · $ plural "$ matches in "$i" " match matches " | | | else | | | color=$GRN icon='✔' status="clean" | | | fi ;; | | | esac | | | printf '\033 2K %s%s%s %-30s %s%s%s\n' "$color" "$icon" "$RS" "${G LABEL i }" "$color" "$status" "$RS" | | | } | | | draw all { | | | $DRAWN -eq 1 && printf '\033 %dA' "$NG" | | | local i | | | for i = 0; i < NG; i++ ; do draw row "$i"; done | | | DRAWN=1 | | | } | | | printf '\n %s%s◆ AI LOG SCOUT%s\n' "$B" "$MAG" "$RS" | | | printf ' %slooking for%s %s%s%s\n\n' "$DIM" "$RS" "$B" "$NEEDLE" "$RS" | | | SECONDS=0 | | | $TTY -eq 1 && { printf '\033 ?25l'; draw all; } | | | for i = 0; i < NG; i++ ; do | | | ${G STATE i } == missing && continue | | | G STATE i =running | | | $TTY -eq 0 && printf ' scanning %s…' "${G LABEL i }" | | | scan group "$i" & | | | WORKER PID=$ | | | while kill -0 "$WORKER PID" 2 /dev/null; do | | | $TTY -eq 1 && draw all | | | TICK=$ TICK + 1 | | | sleep 0.1 | | | done | | | wait "$WORKER PID" 2 /dev/null | | | WORKER PID= | | | G STATE i =done | | | $TTY -eq 0 && printf ' %s\n' "$ plural "$ files in "$i" " file " | | | done | | | $TTY -eq 1 && { draw all; printf '\033 ?25h'; } | | | ELAPSED=$SECONDS | | | --- Results ------------------------------------------------------------------------- | | | pretty path { local h="~"; printf "%s" "${1/ $HOME/$h}"; } | | | VS Code-style workspaceStorage/