Hide what matters before you paste.
Cuddio hides secrets and PII (personally identifiable information) in logs, .env files and MySQL, PostgreSQL and SQLite dumps before you share them with AI tools. Values become placeholders like <EMAIL_1>, so the AI can still tell them apart, and cuddio restore puts the real values back into its answer.
Install
One binary for Linux, macOS and Windows (amd64 and arm64), with no runtime to install. Or download it from GitHub Releases.
brew install phoenix1331/tap/cuddio
scoop bucket add phoenix1331 https://github.com/phoenix1331/scoop-bucket scoop install cuddio
go install github.com/phoenix1331/cuddio@latest
Homebrew, Scoop and the release download need nothing else installed. go install needs Go 1.26 or later, and puts cuddio in Go's bin folder ($(go env GOPATH)/bin), which needs to be on your PATH; the README has step-by-step instructions.
Quick start
$ cat storage/logs/laravel.log | cuddio # clean stdin to stdout $ cuddio app.log -o app.clean.log # write to a new file, never the input $ cuddio --clip # clean the clipboard in place
Try it on the fake files in examples/, which have a command for each.
Why Cuddio
Developers paste logs, stack traces, config and database dumps into AI chats every day, and those often hold API keys and passwords as well as customers' emails, phone numbers and names. Sending personal data to a third-party AI service can breach UK GDPR and company policy, even when nobody meant any harm. Cuddio cleans the text first, on your machine.
Reversible placeholders
The same value always gets the same token, so the AI can reason about which customer is which. cuddio restore swaps the real values back into its answer.
SQL-aware
Reads CREATE TABLE and INSERT columns, hides every value in sensitive columns, and keeps the dump valid SQL. Fake mode writes realistic values so it still imports.
Clipboard, including WSL
cuddio --clip cleans whatever you just copied, with UTF-8 kept intact on Windows, WSL, macOS and Linux.
Laravel and PHP aware
APP_KEY, .env files, PDO DSN passwords, log context payloads and session tables, without being PHP-only.
Built for CI
--check exits 1 when anything would be hidden and prints counts only. Values are never printed in errors or reports.
Single binary, no network
No Python or Node runtime. The test suite fails if any network code is linked into the binary.
Alongside other tools
Cuddio is not the only tool in this space, and it may not be the right one for you.
| Tool | Language | Focus |
|---|---|---|
redacted | Go | Claude Code hook that scrubs secrets from tool output |
redactcli | Python | Redacts secrets from agent output, logs, diffs and CI |
| ShareClean | Python | Cleans logs, stack traces and config before sharing |
| gitleaks | Go | Scans repositories for secrets; Cuddio vendors its rules |
| Cuddio | Go | Secrets and PII in text, logs and SQL dumps, with reversible placeholders, column-aware SQL and the clipboard |
What it does
SQL dumps
MySQL and MariaDB, PostgreSQL (including COPY blocks) and SQLite dumps, streamed in bounded memory. Every value in a sensitive column is hidden whole, pattern rules still run on every string, and the output stays valid SQL. The dialect is detected from the dump's header.
$ cuddio dump.sql -o dump.clean.sql $ cuddio dump.sql --mode fake # realistic fakes $ cuddio dump.sql --dialect postgres
Put the real values back
Clean with --map to keep the placeholder mapping for the session: an owner-only file in your cache directory that expires after an hour. Paste the AI's reply into cuddio restore and the real values come back. Placeholders Cuddio never handed out are left alone.
$ cuddio app.log --map $ cuddio restore < ai-answer.md $ cuddio forget # delete it now
Clean the clipboard
Copy, clean, paste. cuddio --clip works on Windows, WSL, macOS and Linux (wl-clipboard, xclip or xsel), and restore --clip puts the values back, kept out of Windows clipboard history.
$ cuddio --clip $ cuddio restore --clip
Checks in CI
--check exits 1 when anything would be hidden; --report counts by rule. Neither ever prints a value, so they are safe in public build logs. Exit code 2 means an error, so a pipeline can tell "would redact" from "broke".
$ cuddio --check fixtures/support-log.txt $ cuddio --report fixtures/support-log.txt $ cuddio rules --category pii
Your own rules
A .cuddio.yaml in your project adds custom rules, allowlist entries and sensitive columns, and switches rules on or off. A global file in your config directory applies everywhere; the project file wins.
Configuration
Cuddio reads cuddio/config.yaml in your config directory (~/.config, ~/Library/Application Support or %AppData%), then the nearest .cuddio.yaml, walking up to the repository root. --config FILE uses a file of your choice instead of the project one. Unknown settings are errors, and errors never repeat values from the file.
# .cuddio.yaml
mode: placeholder
rules:
enable: [uk-postcode]
disable: [high-entropy-assignment]
custom:
- id: internal-host
category: secret
label: HOST
pattern: '[a-z]+\.internal\.example\.com'
allowlist:
values: [hello@example.com]
patterns: ['^test-']
sql:
columns: [notes]
tables:
orders: [delivery_instructions]
mapping:
ttl: 30m
| Key | Default | Meaning |
|---|---|---|
mode | placeholder | placeholder (reversible tokens) or fake (realistic fake values); --mode overrides it |
placeholder.format | <{label}_{n}> | Token format; must contain {label} and {n} |
rules.enable, rules.disable | none | Rule IDs, categories (secret, pii) or gitleaks; IDs override categories |
rules.custom | none | Your own rules: id, category, label and an RE2 pattern, with an optional (?P<value>...) group to hide only part of a match |
allowlist.values, allowlist.patterns | none | Values never hidden, exactly or by RE2 pattern; reserved domains, loopback addresses and vendor documentation keys are always allowed |
sql.columns | none | Extra sensitive columns in every table |
sql.tables | none | Extra sensitive columns per table |
mapping.ttl | 1h | How long a --map mapping lasts |
mapping.disabled | false | Refuse to store mappings; either file can set it and neither can unset it |
What Cuddio detects
242 rules: 29 of Cuddio's own, and 213 derived from gitleaks. cuddio rules lists them all with their state under your config.
Personal data (PII)
| Rule | Finds | Placeholder | Default |
|---|---|---|---|
e164-phone | International numbers written in E.164 (+ and country code) | <PHONE_1> | on |
email | Email addresses, including URL-encoded ones (alice%40example.com) | <EMAIL_1> | on |
home-directory | The username in /home/x, /Users/x and C:\Users\x | <USER_1> | on |
ip-address | Public IPv4 and IPv6 addresses | <IP_1> | on |
payment-card | Visa, Mastercard, Amex and Discover numbers that pass the Luhn check | <CARD_1> | on |
private-ip-address | Private, link-local and CGNAT addresses | <IP_1> | off |
uk-ni-number | UK National Insurance numbers, with HMRC’s never-issued prefixes rejected | <NI_NUMBER_1> | on |
uk-phone | UK phone numbers, national and +44 forms | <PHONE_1> | on |
uk-postcode | UK postcodes (off by default: short codes in logs often look like them) | <POSTCODE_1> | off |
Sensitive SQL columns
In SQL dumps, every value in these columns is hidden, whatever it looks like. Each also matches with a prefix (billing_email, customer_first_name), except name, which matches only exactly so product_name is left alone.
email password phone mobile name first_name last_name full_name billing_name shipping_name customer_name contact_name display_name address postcode dob date_of_birth ip_address token remember_token api_token secret two_factor_secret two_factor_recovery_codes sessions.id sessions.payload
Secrets
| Rule | Finds | Placeholder | Default |
|---|---|---|---|
anthropic-key | Anthropic API keys | <ANTHROPIC_KEY_1> | on |
authorization-header | Authorization header values | <AUTH_1> | on |
aws-access-key | AWS access key IDs | <AWS_KEY_1> | on |
aws-secret-key | AWS secret access keys | <AWS_SECRET_1> | on |
bearer-token | Bearer tokens | <TOKEN_1> | on |
cookie | Cookie and Set-Cookie values | <COOKIE_1> | on |
github-token | GitHub personal access, OAuth, app and fine-grained tokens | <GITHUB_TOKEN_1> | on |
google-api-key | Google API keys and OAuth client secrets | <GOOGLE_KEY_1> | on |
high-entropy-assignment | Long, random-looking assigned values | <SECRET_1> | on |
jwt | JSON Web Tokens | <JWT_1> | on |
laravel-app-key | Laravel APP_KEY | <APP_KEY_1> | on |
openai-key | OpenAI API keys | <OPENAI_KEY_1> | on |
private-key | PEM private key blocks | <PRIVATE_KEY_1> | on |
secret-assignment | Values of .env keys that name a secret, commented-out lines included | <SECRET_1> | on |
secret-field | Secret fields in JSON and PHP arrays ("api_key": "...") | <SECRET_1> | on |
slack-token | Slack bot, user and app tokens | <SLACK_TOKEN_1> | on |
slack-webhook | Slack incoming webhook URLs | <SLACK_WEBHOOK_1> | on |
stripe-key | Stripe secret, restricted and webhook signing keys | <STRIPE_KEY_1> | on |
token-query-param | Tokens, keys and signatures in query strings | <TOKEN_1> | on |
url-password | Passwords in URLs and DSNs (mysql://app:pw@db, password= in PDO DSNs) | <PASSWORD_1> | on |
213 rules derived from gitleaks
Generated from gitleaks' default configuration, with its entropy checks and allowlists. Switch them all off with rules: {disable: [gitleaks]}, or one at a time by ID. Rules derived from gitleaks, Copyright (c) 2019 Zachary Rice, used under the MIT licence.
1password-secret-key 1password-service-account-token adafruit-api-key adobe-client-id adobe-client-secret age-secret-key airtable-api-key airtable-personnal-access-token algolia-api-key alibaba-access-key-id alibaba-secret-key anthropic-admin-api-key anthropic-api-key artifactory-api-key artifactory-reference-token asana-client-id asana-client-secret atlassian-api-token authress-service-client-access-key aws-access-token aws-amazon-bedrock-api-key-long-lived aws-amazon-bedrock-api-key-short-lived azure-ad-client-secret beamer-api-token bitbucket-client-id bitbucket-client-secret bittrex-access-key bittrex-secret-key cisco-meraki-api-key clickhouse-cloud-api-secret-key clojars-api-token cloudflare-api-key cloudflare-global-api-key cloudflare-origin-ca-key codecov-access-token cohere-api-token coinbase-access-token confluent-access-token confluent-secret-key contentful-delivery-api-token curl-auth-header curl-auth-user databricks-api-token datadog-access-token defined-networking-api-token digitalocean-access-token digitalocean-pat digitalocean-refresh-token discord-api-token discord-client-id discord-client-secret doppler-api-token droneci-access-token dropbox-api-token dropbox-long-lived-api-token dropbox-short-lived-api-token duffel-api-token dynatrace-api-token easypost-api-token easypost-test-api-token etsy-access-token facebook-access-token facebook-page-access-token facebook-secret fastly-api-token finicity-api-token finicity-client-secret finnhub-access-token flickr-access-token flutterwave-encryption-key flutterwave-public-key flutterwave-secret-key flyio-access-token frameio-api-token freshbooks-access-token github-app-token github-fine-grained-pat github-oauth github-pat github-refresh-token gitlab-cicd-job-token gitlab-deploy-token gitlab-feature-flag-client-token gitlab-feed-token gitlab-incoming-mail-token gitlab-kubernetes-agent-token gitlab-oauth-app-secret gitlab-pat gitlab-pat-routable gitlab-ptt gitlab-rrt gitlab-runner-authentication-token gitlab-runner-authentication-token-routable gitlab-scim-token gitlab-session-cookie gitter-access-token gocardless-api-token grafana-api-key grafana-cloud-api-token grafana-service-account-token harness-api-key hashicorp-tf-api-token heroku-api-key heroku-api-key-v2 hubspot-api-key huggingface-access-token huggingface-organization-api-token infracost-api-token intercom-api-key intra42-client-secret jfrog-api-key jfrog-identity-token jwt-base64 kraken-access-token kucoin-access-token kucoin-secret-key launchdarkly-access-token linear-api-key linear-client-secret linkedin-client-id linkedin-client-secret lob-api-key lob-pub-api-key looker-client-id looker-client-secret mailchimp-api-key mailgun-private-api-token mailgun-pub-key mailgun-signing-key mapbox-api-token mattermost-access-token maxmind-license-key messagebird-api-token messagebird-client-id microsoft-teams-webhook netlify-access-token new-relic-browser-api-token new-relic-insert-key new-relic-user-api-id new-relic-user-api-key notion-api-token npm-access-token nytimes-access-token octopus-deploy-api-key okta-access-token openai-api-key openshift-user-token perplexity-api-key plaid-api-token plaid-client-id plaid-secret-key planetscale-api-token planetscale-oauth-token planetscale-password postman-api-token prefect-api-token privateai-api-token pulumi-api-token pypi-upload-token rapidapi-access-token readme-api-token rubygems-api-token scalingo-api-token sendbird-access-id sendbird-access-token sendgrid-api-token sendinblue-api-token sentry-access-token sentry-org-token sentry-user-token settlemint-application-access-token settlemint-personal-access-token settlemint-service-access-token shippo-api-token shopify-access-token shopify-custom-access-token shopify-private-app-access-token shopify-shared-secret sidekiq-secret sidekiq-sensitive-url slack-app-token slack-bot-token slack-config-access-token slack-config-refresh-token slack-legacy-bot-token slack-legacy-token slack-legacy-workspace-token slack-user-token slack-webhook-url snyk-api-token sonar-api-token sourcegraph-access-token square-access-token squarespace-access-token stripe-access-token sumologic-access-id sumologic-access-token telegram-bot-api-token travisci-access-token twilio-api-key twitch-api-token twitter-access-secret twitter-access-token twitter-api-key twitter-api-secret twitter-bearer-token typeform-api-token vault-batch-token vault-service-token yandex-access-token yandex-api-key yandex-aws-access-token zendesk-secret-keySecurity and your data
Nothing is sent
Cuddio never makes a network connection. The tests fail if a network package is imported or any network code is linked into the binary.
Your input is never changed
Input files are only read. -o writes a new file through a temporary file and refuses the input itself, even through a link.
Mappings are opt-in
Only --map stores anything: an owner-only, unencrypted file in your cache directory that expires after an hour. cuddio forget deletes it now.
Values stay out of output
Errors, --check and --report never print a matched value, and config errors never repeat what is in your config.
Use at your own risk. Redaction is best effort: a clean result is not a guarantee that no secret or personal data remains, so review the output before you share it. Cuddio reduces the risk of sharing personal data; it does not by itself make sharing it compliant with UK GDPR. It does not detect names or addresses in free text, or secrets with no recognisable shape. The copy you made before running --clip stays in any clipboard history.
Details, and how to report a problem privately, are in SECURITY.md.