A zero-dependency Python library that detects and redacts secrets from any text — before you log, share, or ship it.
What is Secret Masker?
Secret Masker is a single-file, stdlib-only Python module that automatically finds and replaces credentials, tokens, and keys buried in any string — config dumps, log lines, API responses, error messages — so they never reach the wrong eyes.
It uses three complementary detection layers to catch what individual regex libraries miss:
| Layer | Method | Example catch |
|---|---|---|
| 1 | 35+ provider-specific regex patterns | ghp_..., sk-proj-..., AKIA... |
| 2 | Keyword heuristic assignments | password = "hunter2", api_key: 'abc' |
| 3 | Shannon entropy analysis | Any high-entropy bare/quoted token |
Each match is replaced with [SECRETS], preserving surrounding context.
Features
- Zero dependencies — pure Python standard library, nothing to
pip install - 35+ provider patterns — AWS, GCP, Azure, GitHub, GitLab, Stripe, Slack, Twilio, SendGrid, Discord, OpenAI, Anthropic, JWT, OAuth, SSH keys, private keys, DB connection strings, and more
- Keyword heuristics — catches
password =,api_key:,secret =,bearer, etc. including typos and natural language ("my password is …") - Entropy analysis — fallback layer flags any high-entropy quoted or bare token not caught above
- Overlap resolution — regex wins over heuristics wins over entropy; right-to-left replacement keeps indices valid
- Structured findings — each detection returns
{type, start, end, snippet}for auditing or downstream processing - Single file — drop
secrets_masker.pyanywhere and import it instantly
Installation
pip install secrets-masker
Then import anywhere in your project:
from secretsmasker import mask_secrets
Requirements: Python 3.8+ · No third-party packages · Zero setup
Install from source
git clone https://github.com/haithmgarallah-ye/Secrets-Masker.git
cd Secrets-Masker
pip install .
Quick Start
from secretsmasker import mask_secrets
text = """
aws_key = AKIAIOSFODNN7EXAMPLE
password = "super$ecret99"
token = ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij
db_url = postgres://admin:hunter2@prod.db.example.com:5432/app
"""
result = mask_secrets(text)
print(result.masked_text)
print(result.findings)
Output:
aws_key = [SECRETS]
password = "[SECRETS]"
token = [SECRETS]
db_url = [SECRETS]
Demo Output
Running python secrets_masker.py against the built-in demo text produces:
======================================================================
MASKED TEXT
======================================================================
# === Cloud keys ===
aws_access_key_id = [SECRETS]
aws_secret_access_key = [SECRETS]
gcp_api_key = [SECRETS]
# === VCS tokens ===
github_pat = [SECRETS]
github_oauth = [SECRETS]
github_app_token = [SECRETS]
gitlab_token = [SECRETS]
npm_token = [SECRETS]
# === Payment ===
stripe_secret = [SECRETS]
stripe_pub = [SECRETS]
# === Communication ===
slack_token = [SECRETS]
slack_webhook = [SECRETS]
twilio_sid = [SECRETS]
twilio_key = [SECRETS]
sendgrid = [SECRETS]
discord_token = [SECRETS]
# === AI / LLM keys ===
openai_legacy = [SECRETS]
openai_new = [SECRETS]
# === Auth tokens ===
jwt = [SECRETS]
Authorization: [SECRETS]
client_secret = "[SECRETS]"
access_token = "[SECRETS]"
# === Private keys ===
[SECRETS]
MIIEowIBAAKCAQEA2a2rwplBQLzHPZe5RJFIsBsTV9MriqBuqhoDWMnFfSS...
-----END RSA PRIVATE KEY-----
ssh_pubkey = [SECRETS]
# === Database connection strings ===
postgres_url = [SECRETS]
mongodb_url = [SECRETS]
# === Keyword assignments ===
password = '[SECRETS]'
api_key = "[SECRETS]"
secret_key = "[SECRETS]"
passphrase = "[SECRETS]"
# === Normal text (should NOT be masked) ===
This is a normal sentence with no secrets.
Visit https://example.com for more info.
username = john_doe
======================================================================
FINDINGS (26 secrets detected)
======================================================================
TYPE CHARS SNIPPET
------------------------------ ---------- ----------------------------------------
AWS_ACCESS_KEY 22-42 'AKIAIOSFODNN7EXAMPLE'
GCP_API_KEY 71-110 'AIzaSyD-9tSrke72I6gb4h6kmXDI123456789AB'
GITHUB_TOKEN 136-176 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij'
GITHUB_OAUTH_TOKEN 196-236 'gho_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij'
GITHUB_APP_TOKEN 259-299 'ghs_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij'
GITLAB_TOKEN 319-339 'glpat-abcdefghijklmnopqrst'
...
Demo Usage — Common Patterns
Each example below shows the raw input and the masked output produced by mask_secrets().
AWS Access Key
from secretsmasker import mask_secrets
text = "aws_access_key_id = AKIAIOSFODNN7EXAMPLE"
print(mask_secrets(text).masked_text)
# aws_access_key_id = [SECRETS]
text = "aws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
print(mask_secrets(text).masked_text)
# aws_secret_access_key = [SECRETS]
GCP API Key
text = 'gcp_key = "AIzaSyD-9tSrke72I6gb4h6kmXDI123456789AB"'
print(mask_secrets(text).masked_text)
# gcp_key = "[SECRETS]"
Azure Client Secret
text = "AZURE_CLIENT_SECRET=Xy8Q~dKpLmN3rVwZ9aBcTuEfGhIjOqRsPvWx0123"
print(mask_secrets(text).masked_text)
# AZURE_CLIENT_SECRET=[SECRETS]
GitHub Tokens
# Personal Access Token (classic)
text = "github_token = ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"
print(mask_secrets(text).masked_text)
# github_token = [SECRETS]
# OAuth token
text = "Authorization: gho_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"
print(mask_secrets(text).masked_text)
# Authorization: [SECRETS]
# Fine-grained PAT
text = "token: github_pat_11ABCDE_" + "A" * 82
print(mask_secrets(text).masked_text)
# token: [SECRETS]
GitLab Token
text = "GITLAB_TOKEN=glpat-abcdefghijklmnopqrst"
print(mask_secrets(text).masked_text)
# GITLAB_TOKEN=[SECRETS]
NPM & PyPI Tokens
text = "NPM_TOKEN=npm_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij"
print(mask_secrets(text).masked_text)
# NPM_TOKEN=[SECRETS]
text = "PYPI_TOKEN=pypi-AgEIcHlwaS5vcmcAA" + "B" * 50
print(mask_secrets(text).masked_text)
# PYPI_TOKEN=[SECRETS]
OpenAI API Key
# New project key format
text = 'OPENAI_API_KEY = "sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"'
print(mask_secrets(text).masked_text)
# OPENAI_API_KEY = "[SECRETS]"
# Legacy format
text = "key = sk-aBcDeFgHiJkLmNoPqRsT T3BlbkFJ aBcDeFgHiJkLmNoPqRsT".replace(" ", "")
print(mask_secrets(text).masked_text)
# key = [SECRETS]
Anthropic API Key
text = "ANTHROPIC_API_KEY=sk-ant-api03-" + "A" * 80
print(mask_secrets(text).masked_text)
# ANTHROPIC_API_KEY=[SECRETS]
Stripe Keys
# Secret key
text = 'stripe_secret = "sk_live_<your_stripe_secret_key>"'
print(mask_secrets(text).masked_text)
# stripe_secret = "[SECRETS]"
# Publishable key
text = 'stripe_pub = "pk_test_4eC39HqLyjWDarjtT1zdp7dc"'
print(mask_secrets(text).masked_text)
# stripe_pub = "[SECRETS]"
Slack Token & Webhook
# Bot token
text = "SLACK_TOKEN=xoxb-<team_id>-<bot_id>-<token>"
print(mask_secrets(text).masked_text)
# SLACK_TOKEN=[SECRETS]
# Incoming webhook
text = "webhook = https://hooks.slack.com/services/T<workspace>/B<channel>/<token>"
print(mask_secrets(text).masked_text)
# webhook = [SECRETS]
Twilio
text = """
TWILIO_ACCOUNT_SID = AC<your_account_sid>
TWILIO_AUTH_TOKEN = SK00000000000000000000000000000000
"""
print(mask_secrets(text).masked_text)
# TWILIO_ACCOUNT_SID = [SECRETS]
# TWILIO_AUTH_TOKEN = [SECRETS]
SendGrid API Key
text = "SENDGRID_API_KEY=SG.ngeVfQFYQlKU0ufo8x5d1A.TwL2iGABf9DHoTf-09kqeF8tAmbihYzrnopbd"
print(mask_secrets(text).masked_text)
# SENDGRID_API_KEY=[SECRETS]
Discord Token & Webhook
# Bot token
text = "DISCORD_TOKEN=MTk4NjIyNDgzNDcxOTI1MjQ4.GkFsE.ok_really_long_secret_xxxxxxxxx"
print(mask_secrets(text).masked_text)
# DISCORD_TOKEN=[SECRETS]
# Webhook URL
text = "hook = https://discord.com/api/webhooks/12345678901234567/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmno"
print(mask_secrets(text).masked_text)
# hook = [SECRETS]
Telegram Bot Token
text = "BOT_TOKEN=1234567890:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi"
print(mask_secrets(text).masked_text)
# BOT_TOKEN=[SECRETS]
JWT & Bearer Token
# JWT
jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U"
text = f"Authorization: Bearer {jwt}"
print(mask_secrets(text).masked_text)
# Authorization: [SECRETS]
# OAuth access token
text = 'access_token = "ya29.a0AfH6SMBx-very-long-google-oauth-token-here"'
print(mask_secrets(text).masked_text)
# access_token = "[SECRETS]"
Database Connection Strings
# PostgreSQL
text = "DATABASE_URL=postgres://admin:hunter2@prod.db.example.com:5432/myapp"
print(mask_secrets(text).masked_text)
# DATABASE_URL=[SECRETS]
# MongoDB
text = "MONGO_URI=mongodb+srv://root:S3cr3tP4ss@cluster0.abc.mongodb.net/prod"
print(mask_secrets(text).masked_text)
# MONGO_URI=[SECRETS]
# MySQL
text = "DB_URL=mysql://app_user:Passw0rd!@mysql.internal:3306/users"
print(mask_secrets(text).masked_text)
# DB_URL=[SECRETS]
# Redis
text = "REDIS_URL=redis://:myredispassword@redis.example.com:6379/0"
print(mask_secrets(text).masked_text)
# REDIS_URL=[SECRETS]
Private Key & SSH
# RSA private key header
text = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----"
print(mask_secrets(text).masked_text)
# [SECRETS]
# MIIEowIBAAKCAQEA...
# -----END RSA PRIVATE KEY-----
# SSH public key (also detected)
text = "ssh_pubkey = ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC user@host"
print(mask_secrets(text).masked_text)
# ssh_pubkey = [SECRETS]
Keyword Heuristics (passwords, api keys, secrets)
# Various assignment styles
text = """
password = "hunter2"
passwd = 'Tr0ub4dor&3'
api_key = abcdefghijklmnopqrstuvwx
api_secret = "MyS3cr3tV4lue!!"
secret_key = "django-insecure-abc123xyz"
client_secret = "oauth_client_secret_value"
auth_token = "Bearer abc123xyz456"
"""
print(mask_secrets(text).masked_text)
# password = "[SECRETS]"
# passwd = '[SECRETS]'
# api_key = [SECRETS]
# api_secret = "[SECRETS]"
# secret_key = "[SECRETS]"
# client_secret = "[SECRETS]"
# auth_token = "[SECRETS]"
# Natural language
text = "my password is Tr0ub4dor&3"
print(mask_secrets(text).masked_text)
# my password is [SECRETS]
DigitalOcean Token
text = "DO_TOKEN=dop_v1_" + "a" * 64
print(mask_secrets(text).masked_text)
# DO_TOKEN=[SECRETS]
Real-world: .env file dump
env = """
APP_ENV=production
DATABASE_URL=postgres://app:Sup3rS3cr3t@db.prod.internal/myapp
REDIS_URL=redis://:redispass123@redis.prod.internal:6379
SECRET_KEY=django-insecure-v3ryL0ngS3cr3tK3yV4lu3Here!!
AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
STRIPE_SECRET_KEY=sk_live_<your_stripe_secret_key>
SENDGRID_API_KEY=SG.ngeVfQFYQlKU0ufo8x5d1A.TwL2iGABf9DHoTf-09kqeF8tAmbihYzrnopbd
SLACK_BOT_TOKEN=xoxb-<team_id>-<bot_id>-<token>
PORT=8080
DEBUG=false
"""
result = mask_secrets(env)
print(result.masked_text)
APP_ENV=production
DATABASE_URL=[SECRETS]
REDIS_URL=[SECRETS]
SECRET_KEY=[SECRETS]
AWS_ACCESS_KEY_ID=[SECRETS]
AWS_SECRET_ACCESS_KEY=[SECRETS]
STRIPE_SECRET_KEY=[SECRETS]
SENDGRID_API_KEY=[SECRETS]
SLACK_BOT_TOKEN=[SECRETS]
PORT=8080
DEBUG=false
Non-sensitive values (
PORT,DEBUG,APP_ENV) pass through untouched.
Real-world: log line sanitisation
import json
from secretsmasker import mask_secrets
log_line = json.dumps({
"event": "user_login",
"user_id": 42,
"api_key": "sk-proj-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
"ip": "203.0.113.5"
})
safe_log = mask_secrets(log_line).masked_text
print(safe_log)
# {"event": "user_login", "user_id": 42, "api_key": "[SECRETS]", "ip": "203.0.113.5"}
Inspecting findings metadata
result = mask_secrets(
"token=ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij "
"password=hunter2ABC!"
)
for f in result.findings:
print(f["type"], "@", f["start"], "→", f["end"], "|", f["snippet"][:20])
# GITHUB_TOKEN @ 6 → 46 | ghp_ABCDEFGHIJKLMNOP
# PASSWORD @ 56 → 67 | hunter2ABC!
The findings list preserves the original detection type and character offsets, useful for audit logs and alerting even though the masked text always shows [SECRETS].
Supported Secret Types
Click to expand — all 35+ provider patterns
| Category | Label | Example format |
|---|---|---|
| Cloud | AWS_ACCESS_KEY |
AKIA[A-Z0-9]{16} |
| Cloud | GCP_API_KEY |
AIza[...]{35} |
| Cloud | AZURE_CLIENT_SECRET |
azure_client_secret = ... |
| Cloud | AWS_SESSION_TOKEN |
FwoGZXIvYXdz... |
| VCS | GITHUB_TOKEN |
ghp_[...]{36} |
| VCS | GITHUB_OAUTH_TOKEN |
gho_[...]{36} |
| VCS | GITHUB_APP_TOKEN |
ghu_/ghs_/ghr_[...]{36} |
| VCS | GITHUB_FINE_GRAINED_TOKEN |
github_pat_[...]{82} |
| VCS | GITLAB_TOKEN |
glpat-[...]{20} |
| VCS | BITBUCKET_TOKEN |
bitbucket_token = ... |
| VCS | NPM_TOKEN |
npm_[...]{36} |
| VCS | PYPI_TOKEN |
pypi-[...]{50+} |
| Payment | STRIPE_SECRET_KEY |
sk_live_[...]{24} |
| Payment | STRIPE_PUBLISHABLE_KEY |
pk_test_[...]{24} |
| Payment | STRIPE_RESTRICTED_KEY |
rk_live_[...]{24} |
| Payment | PAYPAL_BRAINTREE_TOKEN |
access_token$production$... |
| Messaging | SLACK_TOKEN |
xoxb-... |
| Messaging | SLACK_WEBHOOK |
hooks.slack.com/services/... |
| Messaging | TWILIO_API_KEY |
SK[0-9a-f]{32} |
| Messaging | TWILIO_ACCOUNT_SID |
AC[a-z0-9]{32} |
| Messaging | SENDGRID_API_KEY |
SG.[...]{22}.[...]{43} |
| Messaging | DISCORD_TOKEN |
[MN][...]{23}.[...]{6}.[...]{27} |
| Messaging | DISCORD_WEBHOOK |
discord.com/api/webhooks/... |
| Messaging | TELEGRAM_BOT_TOKEN |
[0-9]{10}:[A-Za-z0-9_-]{35} |
| Messaging | MAILGUN_API_KEY |
key-[...]{32} |
| Messaging | MAILCHIMP_API_KEY |
[0-9a-f]{32}-us[0-9]{2} |
| AI / LLM | OPENAI_API_KEY |
sk-proj-... / sk-[...]T3BlbkFJ[...] |
| AI / LLM | ANTHROPIC_API_KEY |
sk-ant-[...]{80+} |
| Auth | JWT_TOKEN |
eyJ[...].eyJ[...].[...] |
| Auth | BEARER_TOKEN |
Bearer eyJ... |
| Auth | OAUTH_CLIENT_SECRET |
client_secret = ... |
| Auth | OAUTH_ACCESS_TOKEN |
access_token = ... |
| Keys | PRIVATE_KEY |
-----BEGIN RSA PRIVATE KEY----- |
| Keys | SSH_PUBLIC_KEY |
ssh-rsa AAAA... |
| Keys | CERTIFICATE |
-----BEGIN CERTIFICATE----- |
| Database | DB_CONNECTION_STRING |
postgres://user:pass@host/db |
| Infra | CLOUDFLARE_API_KEY |
v1.0-[0-9a-f]{24}-... |
| Infra | DATADOG_API_KEY |
datadog_api_key = [0-9a-f]{32} |
| Infra | HEROKU_API_KEY |
UUID format |
| Infra | DIGITALOCEAN_TOKEN |
dop_v1_[0-9a-f]{64} |
| CI/CD | CIRCLECI_TOKEN |
circle-token: [0-9a-f]{40} |
| Heuristic | keyword match | api_key = "...", password = '...' |
| Entropy | entropy match | Any high-entropy bare/quoted token |
API Reference
mask_secrets(text: str) -> MaskResult
The single public function. Runs all three detection layers and returns a MaskResult.
from secretsmasker import mask_secrets, MaskResult
result: MaskResult = mask_secrets("token = ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghij")
result.masked_text
# → "token = [SECRETS]"
result.findings
# → [{"type": "GITHUB_TOKEN", "start": 8, "end": 49, "snippet": "ghp_ABCDE..."}]
MaskResult
| Attribute | Type | Description |
|---|---|---|
.masked_text |
str |
Input with all secrets replaced by [SECRETS] |
.findings |
list[dict] |
One entry per detected secret, sorted by position |
Finding dict
| Key | Type | Description |
|---|---|---|
type |
str |
Label e.g. "AWS_ACCESS_KEY" |
start |
int |
Character offset (inclusive) in original text |
end |
int |
Character offset (exclusive) in original text |
snippet |
str |
The literal matched value (for audit logs) |
How It Works
Input text
│
▼
┌─────────────────────────────────────────────┐
│ Layer 1 — Regex Patterns │
│ 35+ compiled provider patterns │
│ Most-specific first; capture group isolates│
│ just the secret value from context │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Layer 2 — Keyword Heuristics │
│ Scans for password=, api_key:, secret = │
│ ~40 keyword variants + typos + nat. lang. │
│ Only the VALUE is flagged; key preserved │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Layer 3 — Entropy Analysis │
│ Shannon entropy on unclaimed spans only │
│ Base64 threshold: 3.5 bits/char │
│ Hex threshold: 3.0 bits/char │
└──────────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ Overlap Resolution │
│ Regex > Heuristic > Entropy │
│ Stable sort; right-to-left replacement │
└──────────────────────┬──────────────────────┘
│
▼
MaskResult(masked_text, findings)
Use Cases
- Safe logging — pass any structured log line through
mask_secrets()before writing to stdout/file/Splunk - CI/CD sanitisation — scrub environment dumps before posting to Slack or GitHub comments
- Data pipelines — redact before inserting into analytics stores
- Debug helpers — print config dicts safely without leaking credentials
- Security scanning — use
.findingsmetadata for alerts and incident reports
License
MIT — see LICENSE for details.
Made with care · zero deps · single file · drop it anywhere
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file secrets_masker-1.0.0.tar.gz.
File metadata
- Download URL: secrets_masker-1.0.0.tar.gz
- Upload date:
- Size: 20.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0437a44f893c9ca0d4a1193e9e9fab7c2da33259cf08f09949e67db410163da4
|
|
| MD5 |
94c0def7b0a7e2e4faf84db7c091296c
|
|
| BLAKE2b-256 |
b463d224ee9a55044b61c237f4bdb9bfd2249e57a0fbfd4dff4b7ae0eff26318
|
File details
Details for the file secrets_masker-1.0.0-py3-none-any.whl.
File metadata
- Download URL: secrets_masker-1.0.0-py3-none-any.whl
- Upload date:
- Size: 15.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
62d9c8d118a737c9b3afab24ac23171c8e1ba33f2509a853710292fd80c72095
|
|
| MD5 |
d7f87b68fa9b6722c82fc0f83c8aa643
|
|
| BLAKE2b-256 |
efd856b481f51d070c2c3b3b1094a5a6280ec6ebf4a92ee30715c7a9bdcbe8ed
|