Originally created by: QY-25123
DestructiveCommandRule lists env in its transparent-wrapper set, for the legitimate env FOO=bar real_command idiom. But a bare env invocation with no trailing command strips down to an empty token list and is silently skipped (if not tokens: continue) before any rule ever sees it. printenv/export/declare -x had no dedicated check at all. All of these read the process environment locally — a common carrier for API keys and tokens — with no pre-execution gate, only a same-turn output scan after the command had already run and the secret had already reached the agent's context.
This was found live: an env call in a Claude-Code-hooked session printed a real API key straight into the conversation. The PostToolUse output scan flagged it after the fact (sensitive_secret_access), but the value had already been read by that point.
Adds _is_environment_dump_segment(), checked on the raw parsed segment before wrapper-stripping (so it sees env/printenv before _argv_from_tokens would otherwise erase them). Recognizes:
env (with its no-op/unset flags: -i, -0, -u VAR, --unset=VAR)printenv (any form, including with a specific variable name — reading the process environment is the concern, not just a full dump)export / export -pdeclare -x / typeset -x with no named variableEnv: drive listing (Get-ChildItem Env: / gci env: / dir env: / ls Env:)All step up to AUTH pre-execution via a new reason code, environment_dump_command — mirroring how sensitive_secret_access already treats a local read of a secret file (.env, id_rsa, …): AUTH, not BLOCK, since there's no external destination yet. Legitimate uses are unaffected: env FOO=bar real_command (env as a wrapper) and export FOO=bar / declare -x FOO=bar (setting one specific variable) still PASS.
Known, documented limits (see the new README "Known limitations" bullet): bare POSIX set and Windows cmd.exe bare set are deliberately out of scope — both list shell variables/functions the same way, but set is also extremely common as set -e/set -euo pipefail in ordinary scripts, so flagging it bare would carry a high false-positive/alert-fatigue cost for a comparatively rare abuse vector (consistent with this project's existing precedent of relaxing a check that fired on the vast majority of benign calls). A literal trailing backslash (dir env:\) isn't in the rule's Windows-path trigger list, so POSIX shlex parsing fails on it first and it falls back to the generic opaque_command AUTH instead — still fails upward, just under a different reason code; the no-backslash form (dir env:) is unaffected.
tests/unit/test_rule_commands.py: 16 new parametrized cases confirming AUTH + environment_dump_command for every recognized dump form (bare env, flag variants, sudo env, all printenv forms, export/export -p, declare -x/typeset -x, all four PowerShell Env: listing spellings), 7 cases confirming legitimate non-dump usage still PASS (env-as-wrapper, setting a named variable, a similarly-named file), one case for the env FOO=bar (no trailing command) edge case per POSIX env semantics, and one confirming the explanation never echoes the raw command text.Also ran (not new, but relevant to this change area): pytest tests/unit/test_objective_guardrail.py (the full rule-combination integration suite CONTRIBUTING.md points to for "Policy / engine rules" changes) — passes unchanged.
Covered: sudo env (wrapper-through-wrapper), env with only no-op/unset flags and no command, PowerShell drive-listing spelled 4 different ways, declare -r FOO (no -x, must not trigger), a file literally named env-notes.txt passed to ls (must not false-positive on the PowerShell drive check).
Deviation from a fully generic fix: bare set (POSIX and cmd.exe) is intentionally NOT covered, for the alert-fatigue reason above — documented as a known limitation rather than silently scoped out.
One design choice worth a maintainer's eye: printenv <SPECIFIC_VAR> (a targeted read, not a full dump) is treated the same as bare printenv — I judged "reads the process environment" as the operative risk rather than "dumps everything," but this is a slightly broader net than a minimal fix would draw, and I'm open to narrowing it if that's not the intended scope for this rule.
Risk: none identified — this only adds a new AUTH step-up path on a narrow, verb-based predicate; it cannot lower any existing verdict, and the new check runs before, not instead of, the existing per-segment classification, so no existing branch's behavior changes.
This change (the finding, the fix, the tests, and this PR description) was developed with Claude Code, with human review and direction throughout. The gap was found live during a Doberman-guarded coding session (an env call printed a real secret with no pre-execution check), then verified against the codebase, fixed, and checked locally (ruff, ruff format --check, lint-imports, the targeted and related pytest suites) before opening this PR. Full CI (Ubuntu 3.11/3.12/3.13, Windows, secret-scan, package-smoke-test) is green on the originating fork PR: https://github.com/QY-25123/Doberman-Core/pull/1
Originally posted by: fu351
Heads-up, and nothing wrong with the change itself: a stack of PRs landed on
maintoday, so this one picked up a merge conflict (when several land at once,CHANGELOG.mdis almost always the culprit). Your env-dump step-up is good and I want it in, so a rebase is all it needs:git fetch && git rebase origin/main, resolve the small conflict, then force-push. Ping me once it is green and I will take the merge. Thanks for riding out the churn.Originally posted by: QY-25123
Rebased onto current
mainand resolved the CHANGELOG conflict — CI is green across the board. Ready for you to take the merge whenever.Ticket changed by: fu351
Originally posted by: fu351
Merged, thanks @QY-25123! The root-cause fix is what makes this one good: bare
envwas being erased by the transparent-wrapper list before any rule could see it, and checking the raw segment before wrapper-stripping fixes the actual mechanism instead of patching a symptom, with the pipeline and&&cases covered for free by the existing segmentation. Forget the rebase I asked for earlier, the conflict kept growing on my side of the fence, so I landed it through [#455] with your commits untouched, keeping main's newer README wording for the Codex installer paragraph plus your new limitations bullet.Related
Tickets:
#455