File of the day
A Claude Code script that finds passwords left in plain text in your files
Slava Sazhin 9 October 2026 3 min read 0 views
Protect your accounts and money: Claude Code finds files with your passwords in plain text, so you remove them in time.
Today's file is a script for Claude Code. A bank password in a note, the Wi-Fi password in meeting minutes, a table of logins in Excel, an API key in a project's .env file: whoever gets a copy of that folder gets the accounts too, from a lost laptop, a shared drive or a backup in the wrong place. The script finds such files, so you can take the passwords out of them first.
How it works
Claude runs find_secrets.py on a folder you name. The script reads every file in it, Word documents (.docx), Excel spreadsheets (.xlsx) and CSV files included, and prints the path of each file that holds a password, an access key or a private key, with what it found there. It never prints the password itself, so the values do not end up in the chat, and it changes nothing. A password counts when a word for password stands next to it, in English, Russian, Portuguese or Serbian (password, пароль, senha, lozinka); a column called Password in a spreadsheet is enough too. A password with no such word nearby is not found. Keys are recognised by their shape: private keys, AWS and GitLab keys, JWT tokens, Google service account files. It skips .git, node_modules and venv folders, and reads only the first 2 MB of a large file.
In our test it named all 21 files we planted: notes with passwords in the four languages, a password table in Word and in Excel, a browser export in CSV, a .env file, a private key, AWS, GitLab and JWT tokens, a database connection string. It passed over placeholders like changeme, ${DB_PASSWORD} and os.environ, and over a sentence like "never share your password". Then we asked Claude, in all four languages, to run it on a test Documents folder and show what it found. In 6 runs out of 6 Claude listed the files and what was in them, opened none of them and offered to look inside only if asked.
What to change
We changed no logic. Two dash characters inside one search pattern became escape codes, so the English file is plain ASCII; it finds exactly the same. Point the script at the folder with your documents, not your whole home folder: a big folder takes minutes, up to a second for each large file, and the home folder also has files that are meant to hold a key, such as the ones in ~/.ssh. To catch a password next to a word in another language, say Passwort, ask Claude to add it to the LABELS line of the script. When it names a file, open the file yourself and move the password to a password manager, and change it if the folder was ever shared. If you ask Claude to open the file, the password goes into the chat.
Install
The note below has the exact requests. The first puts the script into ~/.claude/scripts and checks that python3 is on the computer. That folder holds Claude's own settings, so Claude asks before it writes there: answer Yes. The second runs it on your Documents folder. The check from the note: a test folder with note.txt holding the line Email password: Kofe2026sad. The script names note.txt and the word password, and does not print Kofe2026sad.
The files
Every file of the day on GitHub#!/usr/bin/env python3
"""Finds files in a folder where a password or an access key is written in plain text.
Run: python3 find_secrets.py <folder>
Prints the file path and the kind of what it found. It never prints the passwords themselves.
It changes nothing, it only reads. It also reads Word documents and Excel spreadsheets.
"""
import html
import os
import re
import sys
import zipfile
# How much to read from one file: only the beginning of a large file is read.
READ_MAX = 2_000_000
# Folders the check does not enter.
SKIP_DIRS = {'.git', 'node_modules', '__pycache__', '.venv', 'venv'}
# The word "password" in English, Russian, Portuguese and Montenegrin.
LABELS = r'password|passwd|pwd|db_pass|\u043f\u0430\u0440\u043e\u043b|senha|lozink'
# Strong signs: only a real key has text like this.
STRONG = [
('private key', re.compile(r'-----BEGIN [A-Z ]*PRIVATE KEY-----')),
('JWT token', re.compile(r'\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.')),
('Google service account key', re.compile(r'"type"\s*:\s*"service_account"')),
('GitLab token', re.compile(r'glpat-[A-Za-z0-9_-]{15,}')),
('AWS key', re.compile(r'\bAKIA[0-9A-Z]{16}\b')),
('the word "password" in a line or cell of its own', re.compile(r'(?im)^[ \t"]*(?:' + LABELS + r')\w{0,30}(?:[ \t]+\w{1,30}){0,2}[ \t"\r]*$')),
]
# Soft signs: a word like "password", secret or api_key, and the value after it.
SOFT = [
('password', re.compile(r'(?i)\b(?:' + LABELS + r')[\w \t"\'-]{0,60}[=:]\s*(["\']?)(\S{6,})')),
('password', re.compile(r'(?i)\b(?:' + LABELS + r')\w{0,30}(?:[ \t]+\S{1,30}){0,6}?'
r'(?:[ \t]*[-\u2013\u2014:=][ \t]*|[ \t]+)(["\']?)((?=\S{0,200}?[\d!@#$%^&*?_+])\S{6,200})')),
('secret', re.compile(r'(?i)\b[A-Z0-9_]*secret[A-Z0-9_]*["\']?\s*[=:]\s*(["\']?)(\S{8,})')),
('access key', re.compile(r'(?i)\b[A-Z0-9_]*(?:api[_-]?key|apikey|access[_-]?key|auth[_-]?'
r'token|bot[_-]?token)[A-Z0-9_]*["\']?\s*[=:]\s*(["\']?)(\S{8,})')),
('password in a connection string', re.compile(r'(?i)(?:Password|Pwd)\s*=\s*([^;\s]{4,});')),
]
# The value is a reference to a variable or a setting, not the password itself.
REFERENCE = re.compile(r"""(?ix)^(
os\.environ|os\.getenv|environ|getenv|process\.env|System\.|Environment\.|configuration|
config\.|self\.|this\.|cfg\.|settings\.|require\(|import\b|input\(|prompt
)""")
BRACKET = re.compile(r'^(\$\{?[A-Za-z_]|%\(|\{\{|\{[A-Za-z_]|<[A-Za-z_]|@[A-Za-z_]|\[)')
# The value is a placeholder from an example, not a password. It is compared as a whole.
DUMMY = re.compile(r'(?ix)^(None|null|nil|true|false|undefined|x{3,}|y{3,}|\*+|\.{3,}|'
r'change_?me|your_?\w*|test|password|secret|dummy|example|sample|'
r'placeholder|\d{1,4}|["\'`]*)$')
def value(m):
g = [x for x in m.groups() if x is not None]
return (g[-1] if g else '').strip('"\'`,;)')
def is_secret(v):
if not v or len(v) < 6:
return False
if REFERENCE.match(v) or BRACKET.match(v):
return False
if DUMMY.fullmatch(v):
return False
return True
def read_text(path):
"""Text of the file. For a Word document or an Excel spreadsheet, the text from their parts, one paragraph or cell per line."""
low = path.lower()
if low.endswith(('.docx', '.xlsx')):
with zipfile.ZipFile(path) as z:
parts = [z.open(n).read(READ_MAX) for n in z.namelist() if n.endswith('.xml')]
text = b'\n'.join(parts).decode('utf-8', 'ignore')
text = re.sub(r'</(?:w:p|w:tc|si|c|row)>', '\n', text)
return html.unescape(re.sub(r'<[^>]+>', '', text))
with open(path, 'rb') as fh:
text = fh.read(READ_MAX).decode('utf-8', 'ignore')
# In a CSV table, each field goes on its own line.
return re.sub(r'[,;\t]', '\n', text) if low.endswith(('.csv', '.tsv')) else text
def signs(path):
"""Kinds of what was found in the file. The values themselves are never returned."""
try:
data = read_text(path)
except Exception:
return []
found = [name for name, rx in STRONG if rx.search(data)]
for name, rx in SOFT:
for m in rx.finditer(data):
if is_secret(value(m)):
found.append(name)
break
return sorted(set(found))
def main():
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
root = sys.argv[1] if len(sys.argv) > 1 else '.'
if not os.path.isdir(root):
sys.exit(f'No such folder: {root}')
total = 0
for folder, dirs, files in os.walk(root):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for f in files:
path = os.path.join(folder, f)
if os.path.islink(path) or not os.path.isfile(path):
continue
found = signs(path)
if found:
total += 1
print(f'{path}: {", ".join(found)}')
print(f'Files with an open password or key: {total}')
if __name__ == '__main__':
main()
# How to find passwords that sit in your files as plain text
1. Ask Claude: "Put the downloaded file find_secrets.py into the folder ~/.claude/scripts and check that python3 is on the computer; if it is not, install it".
2. Ask Claude: "Run ~/.claude/scripts/find_secrets.py on the folder $HOME/Documents and show me what it found". Instead of Documents, name the folder where your work files are.
3. The script prints the path of each file it found and what is in it: a password, an access key or a private key. It looks for a password next to the word password, and it will not find a password with no such word next to it. It also reads Word documents, Excel spreadsheets and CSV files. It does not show the passwords themselves and changes nothing in the files.
Check:
1. Ask Claude: "Create the folder $HOME/secrets-test, put a file note.txt in it with the line Email password: Kofe2026sad and run ~/.claude/scripts/find_secrets.py on this folder". The script prints the path of the file note.txt and the word "password", and does not show the password itself.
2. Ask Claude: "Delete the folder $HOME/secrets-test".
Was this article worth your time?
A new masterclass every week
On camera, a practitioner walks through a setup that works and hands over every file in it. $11 a month.
Latest recording · 30 September 2026 Claude apologised and promised never to do it again? Do not trust promises, set up hooks Slava Sazhin