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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant