plugin-scanner 3.x flags password: "<password>" and a "[redacted ...]" constant as high severity HARDCODED_SECRET when the file sits outside a docs, tests or examples path.
This is the same shape as #2811, which fixed the ${VAR} case. The < and [ placeholder forms still reach the report.
Cause
_looks_like_placeholder_secret in checks/security.py already recognises both forms:
if _looks_like_interpolated_secret(normalized) or normalized.startswith(("<", "[")):
return True
But in _should_skip_secret_match it is gated behind the surface test, and the early return fires first:
if not _is_example_surface(relative_path):
return False # non-docs path stops here
if _looks_like_placeholder_secret(candidate): # never asked
return True
_looks_like_interpolated_secret was hoisted above that gate, which is why ${VAR} is now clean; startswith(("<", "[")) was left below it.
Confirmed against the installed package:
$ python -c "from codex_plugin_scanner.checks.security import _looks_like_placeholder_secret as p, _is_example_surface as e; ..."
'<password>' -> placeholder? True
'[redacted - call ethora-app-...]' -> placeholder? True
src/tools.ts -> example surface? False
src/redact.ts -> example surface? False
So the value is correctly identified as a placeholder, and the answer is discarded because of where the file lives.
Repro
Any plugin with a TypeScript source file outside docs/tests:
// src/a.ts
const nextCall = { email: "user@example.com", password: "<password>" }
export const REDACTED_APP_TOKEN = "[redacted - call the reveal tool for appToken]"
plugin-scanner scan .
# HARDCODED_SECRET high: Potential secret material was detected in src/a.ts
Why it matters beyond one repo
fail_on_severity: high in the awesome-ai-plugins catalog gate means any project that writes password: "<password>" in an example payload, or ships a redaction placeholder constant, cannot be listed without rewording correct code. The second case is the awkward one: on our server the flagged line is the placeholder belonging to the module whose only job is stripping credentials out of tool results before they reach a model's context, so the check fires on the fix rather than the problem.
Real-world instance: dappros/ethora-mcp-server scores 85/100 with 0 critical, 0 medium after acting on the other findings (pinning every action to a SHA and adding Dependabot, both fair), and is blocked only by these two.
Suggested fix
Hoist the unambiguous bracketed forms next to the interpolated check, keeping the rest of the placeholder heuristics scoped to example surfaces as they are today:
candidate = _extract_secret_candidate(detector, match)
if _looks_like_interpolated_secret(candidate):
return True
if _normalize_secret_candidate(candidate).startswith(("<", "[")):
return True
That stays narrow: <...> and [...] are not shapes a real credential takes, and a value that is genuinely secret would not survive _normalize_secret_candidate starting with either. Happy to send this as a PR with a test if the approach suits you.
plugin-scanner 3.x flags
password: "<password>"and a"[redacted ...]"constant as high severityHARDCODED_SECRETwhen the file sits outside a docs, tests or examples path.This is the same shape as #2811, which fixed the
${VAR}case. The<and[placeholder forms still reach the report.Cause
_looks_like_placeholder_secretinchecks/security.pyalready recognises both forms:But in
_should_skip_secret_matchit is gated behind the surface test, and the early return fires first:_looks_like_interpolated_secretwas hoisted above that gate, which is why${VAR}is now clean;startswith(("<", "["))was left below it.Confirmed against the installed package:
So the value is correctly identified as a placeholder, and the answer is discarded because of where the file lives.
Repro
Any plugin with a TypeScript source file outside docs/tests:
Why it matters beyond one repo
fail_on_severity: highin the awesome-ai-plugins catalog gate means any project that writespassword: "<password>"in an example payload, or ships a redaction placeholder constant, cannot be listed without rewording correct code. The second case is the awkward one: on our server the flagged line is the placeholder belonging to the module whose only job is stripping credentials out of tool results before they reach a model's context, so the check fires on the fix rather than the problem.Real-world instance: dappros/ethora-mcp-server scores 85/100 with 0 critical, 0 medium after acting on the other findings (pinning every action to a SHA and adding Dependabot, both fair), and is blocked only by these two.
Suggested fix
Hoist the unambiguous bracketed forms next to the interpolated check, keeping the rest of the placeholder heuristics scoped to example surfaces as they are today:
That stays narrow:
<...>and[...]are not shapes a real credential takes, and a value that is genuinely secret would not survive_normalize_secret_candidatestarting with either. Happy to send this as a PR with a test if the approach suits you.