DEV Community

atani
atani

Posted on • Originally published at zenn.dev

mysh — A MySQL Connection Manager That Auto-Masks PII in Query Output

The Problem

You get an error notification filed as an issue. You ask Claude Code to investigate the codebase, and it turns out you need real data to narrow down the root cause. You write a SQL query, run it manually, and paste the results back to Claude Code.

But if the query results contain personal information — emails, phone numbers, names — you can't just hand them over as-is. Every time, you either rewrite the SQL to exclude sensitive columns or manually redact PII from the output before pasting. Do this dozens of times a day and you'll inevitably forget once. And once is all it takes.

Why Existing Tools Don't Solve This

I looked for a MySQL client with output masking. None of the popular ones had it.

Tool Masking? Notes
mycli No Focused on syntax highlighting and autocomplete
DataGrip No Can hide columns, but doesn't mask output
TablePlus / DBeaver No Same story across GUI clients
MySQL Enterprise Server-side only mask_inner() etc. — requires Enterprise Edition

MySQL Enterprise Edition has server-side masking functions, but they're paid and require DB-level configuration. No client-side tool offered output masking with AI workflows in mind.

So I Built mysh

If it doesn't exist, build it. I wrote mysh, a MySQL connection manager that auto-masks query output, in Go. Built it with Claude Code in two days.

Masking demo
Production connections are masked automatically. Development connections show raw data.

How It Works: Environment × Output Target

mysh determines whether to mask based on two factors: the connection's environment and the output destination (terminal vs. pipe).

env Terminal (human) Pipe/capture (AI)
production Auto-mask Auto-mask
staging Raw Auto-mask
development Raw Raw

Production connections are always masked regardless of output target. If you need raw data, use the --raw flag — but it requires interactive confirmation:

$ mysh run prod-db -e "SELECT * FROM users" --raw
⚠ Raw output requested for production connection "prod-db".
  Masking will be disabled. Continue? [y/N]:
Enter fullscreen mode Exit fullscreen mode

This confirmation only works on a TTY (terminal). AI tools and scripts run in non-TTY mode, so they physically cannot respond to the prompt — making it impossible for them to bypass masking.

Configuring Masked Columns

When adding a connection, you specify which columns to mask. Both exact matches and wildcards are supported:

Columns to mask (comma-separated, wildcards OK) [email,phone,*password*,*secret*,*token*,*address*]:
Enter fullscreen mode Exit fullscreen mode

For production and staging environments, sensible defaults are suggested — just press Enter to cover common PII columns. Setting *pass* masks any column containing "pass" in its name.

Pro tip: show your schema to Claude Code and ask it to pick which columns should be masked. It catches things you might miss.

Masking Examples

Type Original Masked
Email alice@example.com a***@example.com
Phone 090-1234-5678 0***
Name Alice A***

Usage

Adding a Connection

Add connection demo

Walk through the setup interactively, or pre-fill fields with CLI flags:

# Fully interactive
mysh add

# Pre-fill with flags (password is always entered interactively)
mysh add --name prod --env prod --db-host 127.0.0.1 --db-user app --db-name myapp \
  --ssh-host bastion.example.com --ssh-user deploy
Enter fullscreen mode Exit fullscreen mode

A connection test runs after setup. If it fails, you can fix the specific field on the spot.

Running Queries

If you only have one connection, the name can be omitted:

mysh run -e "SELECT COUNT(*) FROM users"  # Inline SQL
mysh run query.sql                          # SQL file
mysh tables                                 # List tables
Enter fullscreen mode Exit fullscreen mode

SSH Tunneling

Connections through a bastion host work with a single command:

mysh tunnel production                                  # Start tunnel
mysh run production -e "SHOW PROCESSLIST"               # Auto-reuses tunnel
mysh tunnel stop production                             # Stop
Enter fullscreen mode Exit fullscreen mode

Security Design

Beyond masking, mysh is designed to minimize the risk of credential leaks:

  • Encrypted passwords — AES-256-GCM encryption with Argon2id key derivation, resistant to GPU brute-force attacks
  • Keychain integration — On macOS, the master password is stored in Keychain so you don't type it every time
  • File permissions — Config files are created with 0600, preventing access by other users
  • No password CLI flags — Passwords cannot be passed as CLI arguments, preventing leaks via shell history or process lists

Technical Notes

Why Go?

Single-binary distribution and easy cross-compilation make Go ideal for CLI tools. The golang.org/x/term package provides TTY detection, which made implementing the environment-aware masking behavior straightforward.

TTY Detection for Masking

term.IsTerminal() checks whether stdout is a terminal or a pipe. Combined with the environment setting, this determines masking behavior. Production always masks regardless; the --raw override additionally checks os.Stdin for TTY to block non-interactive bypass.

func (c *Connection) ShouldMask(isTTY bool) bool {
    if c.Env == "production" {
        return true
    }
    if c.Env == "development" {
        return false
    }
    return !isTTY // staging: mask only when piped
}
Enter fullscreen mode Exit fullscreen mode

Usage Tips

  • Connect with a READ-only user — Combined with masking, this also prevents accidental data modification
  • Use mysh for development too — Masking isn't applied in dev, so it doesn't interfere with testing. Using mysh everywhere means you don't switch tools when investigating production
  • Let AI pick masked columns — Run SHOW COLUMNS FROM table_name and ask Claude Code to identify PII columns. It's thorough

Limitations

  • Column-name-based masking — Irregular column names (col1, data, etc.) require manual configuration
  • MySQL only — No PostgreSQL or other database support
  • Masking format is fixed: first character preserved, rest replaced with ***. No customization
  • Large result sets (tens of thousands of rows) incur overhead from the masking pass

Try It

Install with Homebrew in 30 seconds:

brew tap atani/tap && brew install mysh
mysh add  # Interactively add a connection
Enter fullscreen mode Exit fullscreen mode

If you find it useful, a star on GitHub would mean a lot.

GitHub: https://github.com/atani/mysh

Top comments (0)