Cuddio
Local only · single binary · no network

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.

A terminal showing two Laravel log lines with an email, phone number, card number, IP address and tokens, then the same lines through Cuddio with each value replaced by a placeholder such as EMAIL_1 and CARD_1.
Two lines of a Laravel log, before and after.
Cuddio /KITH-yo/ Welsh for to hide. The dd is the th in this.

Install

One binary for Linux, macOS and Windows (amd64 and arm64), with no runtime to install. Or download it from GitHub Releases.

Homebrew
brew install phoenix1331/tap/cuddio
Scoop (Windows)
scoop bucket add phoenix1331 https://github.com/phoenix1331/scoop-bucket
scoop install cuddio
Go 1.26 or later
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.

ToolLanguageFocus
redactedGoClaude Code hook that scrubs secrets from tool output
redactcliPythonRedacts secrets from agent output, logs, diffs and CI
ShareCleanPythonCleans logs, stack traces and config before sharing
gitleaksGoScans repositories for secrets; Cuddio vendors its rules
CuddioGoSecrets 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
Cuddio cleaning a MySQL users row: names, emails and password hashes become placeholders, then realistic fake values with --mode fake, then --check reports 38 values would be hidden.

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
Cuddio cleaning a log with --map, an AI answer that refers to EMAIL_1 and PASSWORD_1, and cuddio restore putting the real addresses and password back.

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
Pasting a database error with an IP address and a DSN password, running cuddio --clip, and pasting again to show placeholders.

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
cuddio --check on an nginx log reports 9 values and exit code 1, --report lists counts per rule, and --check on a clean file reports nothing to hide with exit code 0.

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.

Adding a .cuddio.yaml with an allowlisted address and a custom rule for internal host names, after which the hosts become HOST placeholders and the allowlisted address is kept.

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
KeyDefaultMeaning
modeplaceholderplaceholder (reversible tokens) or fake (realistic fake values); --mode overrides it
placeholder.format<{label}_{n}>Token format; must contain {label} and {n}
rules.enable, rules.disablenoneRule IDs, categories (secret, pii) or gitleaks; IDs override categories
rules.customnoneYour 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.patternsnoneValues never hidden, exactly or by RE2 pattern; reserved domains, loopback addresses and vendor documentation keys are always allowed
sql.columnsnoneExtra sensitive columns in every table
sql.tablesnoneExtra sensitive columns per table
mapping.ttl1hHow long a --map mapping lasts
mapping.disabledfalseRefuse 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)

RuleFindsPlaceholderDefault
e164-phoneInternational numbers written in E.164 (+ and country code)<PHONE_1>on
emailEmail addresses, including URL-encoded ones (alice%40example.com)<EMAIL_1>on
home-directoryThe username in /home/x, /Users/x and C:\Users\x<USER_1>on
ip-addressPublic IPv4 and IPv6 addresses<IP_1>on
payment-cardVisa, Mastercard, Amex and Discover numbers that pass the Luhn check<CARD_1>on
private-ip-addressPrivate, link-local and CGNAT addresses<IP_1>off
uk-ni-numberUK National Insurance numbers, with HMRC’s never-issued prefixes rejected<NI_NUMBER_1>on
uk-phoneUK phone numbers, national and +44 forms<PHONE_1>on
uk-postcodeUK 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

RuleFindsPlaceholderDefault
anthropic-keyAnthropic API keys<ANTHROPIC_KEY_1>on
authorization-headerAuthorization header values<AUTH_1>on
aws-access-keyAWS access key IDs<AWS_KEY_1>on
aws-secret-keyAWS secret access keys<AWS_SECRET_1>on
bearer-tokenBearer tokens<TOKEN_1>on
cookieCookie and Set-Cookie values<COOKIE_1>on
github-tokenGitHub personal access, OAuth, app and fine-grained tokens<GITHUB_TOKEN_1>on
google-api-keyGoogle API keys and OAuth client secrets<GOOGLE_KEY_1>on
high-entropy-assignmentLong, random-looking assigned values<SECRET_1>on
jwtJSON Web Tokens<JWT_1>on
laravel-app-keyLaravel APP_KEY<APP_KEY_1>on
openai-keyOpenAI API keys<OPENAI_KEY_1>on
private-keyPEM private key blocks<PRIVATE_KEY_1>on
secret-assignmentValues of .env keys that name a secret, commented-out lines included<SECRET_1>on
secret-fieldSecret fields in JSON and PHP arrays ("api_key": "...")<SECRET_1>on
slack-tokenSlack bot, user and app tokens<SLACK_TOKEN_1>on
slack-webhookSlack incoming webhook URLs<SLACK_WEBHOOK_1>on
stripe-keyStripe secret, restricted and webhook signing keys<STRIPE_KEY_1>on
token-query-paramTokens, keys and signatures in query strings<TOKEN_1>on
url-passwordPasswords 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-key

Security 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.