Skip to content

Draft: add -m/--mask flag to dotenv list (sensitivity-gated) - #688

Closed
maorq08 wants to merge 1 commit into
theskumar:mainfrom
maorq08:fm/dotenv-cli-scout
Closed

Draft: add -m/--mask flag to dotenv list (sensitivity-gated)#688
maorq08 wants to merge 1 commit into
theskumar:mainfrom
maorq08:fm/dotenv-cli-scout

Conversation

@maorq08

@maorq08 maorq08 commented Aug 20, 2026

Copy link
Copy Markdown

⚠️ Draft — not a contribution proposal

This is a draft PR opened for internal review only. It is not being proposed as an
official contribution to python-dotenv and is not ready to merge. Please do not review
or merge it as an upstream contribution.

What this adds

A -m/--mask flag on dotenv list that redacts values whose key looks sensitive,
so secrets don't get printed in plaintext by dotenv list (e.g. in a terminal recording,
CI log, or screen share).

$ dotenv list --mask
API_KEY=ab****gh
DB_PASSWORD=****
PORT=8080
HOST=example.com

Masking rule

Two independent pieces, both gated per-key:

  1. Sensitivity heuristic_is_sensitive_key(key) does a case-insensitive substring
    match against a fixed keyword list: KEY, SECRET, TOKEN, PASSWORD, PASSWD,
    PWD, CREDENTIAL, AUTH, PRIVATE, ACCESS, CERT, DSN, CONNECTION_STRING,
    CONN_STRING. This is the same substring-match convention used by mise-en-place,
    Apache Airflow's keyword-based masking, and gitleaks' keyword list — e.g. MY_API_KEY
    matches on KEY, DB_PASSWORD matches on PASSWORD. A key that doesn't match prints
    its value completely unchanged, even with --mask on.
  2. Length-based partial reveal_mask_value(value), applied only to values whose
    key matched the heuristic:
    • values longer than 4 characters keep the first and last 2 characters, e.g.
      MY_SECRET_KEYMY****EY
    • values of 4 characters or fewer become **** entirely

None/unset values (a bare key with no =value in the .env file) are left untouched
either way, so --format=json's null output and the plain formats' existing
"skip if value is None" behavior are unchanged.

Format-agnostic by construction

The masking transform is applied once, to the values dict returned by
dotenv_values(stream=stream), immediately after that call and before the --format
branch in list_values (src/dotenv/cli.py). That means simple (default), json,
shell, and export output are all masked consistently — the transform isn't
duplicated per format branch.

Tests

Added to tests/test_cli.py, following the file's existing cli/dotenv_path fixture
and CliRunner-based style:

  • Existing test_list_mask table extended across all four --format values (values

    4 chars, ≤4 chars, and a bare/None key).

  • test_list_mask_non_sensitive_key_unmasked — the key case proving the heuristic
    actually gates masking rather than masking everything (PORT=8080 prints unmasked).
  • test_list_mask_short_flag — covers the -m alias.
  • test_list_mask_empty_value — an explicit KEY= (empty string, distinct from a bare
    key with no = at all) masks to ****.
  • test_list_mask_five_char_boundary — a 5-character value is the first to cross into
    the partial-reveal branch.
  • test_list_mask_custom_file--mask combined with an explicit non-default
    --file path.
  • test_list_mask_mixed_sensitive_and_non_sensitive (+ a --format=json variant) — a
    sensitive and a non-sensitive key in the same file, so per-key gating is visible in
    one result rather than only single-key cases.

Full suite: 266 passed, 1 skipped locally (plus 3 pre-existing failures in
test_run_with_invalid_cmd/test_run_with_version/test_run_with_command_flags that
reproduce identically on unmodified main — they're a macOS BSD-printenv environment
incompatibility, unrelated to this change). ruff check and mypy are clean on both
changed files.

CHANGELOG

Added an ## [Unreleased] / ### Added entry describing the sensitivity-gated behavior.

Masks displayed values whose key looks sensitive (case-insensitive
substring match against a keyword list: KEY, SECRET, TOKEN, PASSWORD,
PASSWD, PWD, CREDENTIAL, AUTH, PRIVATE, ACCESS, CERT, DSN,
CONNECTION_STRING, CONN_STRING - the convention used by mise-en-place,
Airflow, and gitleaks). Values longer than 4 chars show first/last 2
chars with **** in between, values of 4 chars or fewer become ****
entirely. Non-sensitive keys and None/unset values are left untouched.
The transform is applied once to the values dict before the --format
branch, so simple/shell/export/json output are all masked consistently.
@maorq08

maorq08 commented Aug 20, 2026

Copy link
Copy Markdown
Author

Closing - this was a draft opened for internal review only, not an upstream contribution proposal.

@maorq08 maorq08 closed this Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant