You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit edad2e8
Browse filesBrowse the repository at this point in the historyBrowse files
fix(secrets): report recognized reads instead of proving they are not reads
Review round 7. Greptile flagged both directions at once — false usage from
reporting a write target, and unmasked secrets from the file-wide shadow flag —
so I traced what the signal actually drives before choosing.
The chain: the compiler's names feed outputSecretPlaintextsByName and the
exact-value matcher, NOT context.resolvedSecretNames, which starts empty. After
execution activateOutputSecretProvenance scans the output and adds only names
whose plaintext actually appeared; those become __resolvedSecretNames, which
tools/index.ts turns into recordResolved calls, which is what the usage trail
reads.
So a compile-time false positive produces no usage row on the ordinary path — it
only hands the matcher a value the code never emits. It does produce one on the
!projection.safe fallback, where the system already over-approximates by design.
A false negative, by contrast, keeps the value out of the matcher entirely, so a
genuinely read secret is never masked on any path.
That asymmetry decides it, so every "prove this is not a read" mechanism is gone:
- javascript.ts: the file-wide shadow flag. A helper declaring its own
environmentVariables discarded genuine reads of the mounted binding everywhere
else in the file — Greptile's security finding, and real.
- python.ts: the allowlist requiring every mention to be a subscript or .get().
Same hole: passing the dict to a function suppressed unrelated reads.
- shell.ts: the rebinding check. It had the same hole in a form nobody flagged —
`echo "$API_KEY"; API_KEY=local` dropped the first read, which is of the real
secret.
What stays is the question of whether the text is code at all — strings, comments,
single quotes, quoted heredocs — plus the receiver check that `other.environment
Variables['K']` is a different object, and JavaScript's node-precise write/delete
exclusion, which cannot suppress a read elsewhere.
Net 215 lines removed across the three detectors and their tests. Docs updated:
the rule is now stated as reporting rather than proving, and that See usage may
occasionally list a secret the code had available but did not read.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: apps/docs/content/docs/en/platform/credentials.mdx
+4-2Lines changed: 4 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -74,12 +74,14 @@ Secret resolution and functional workflow behavior are unchanged: blocks, tools,
74
74
Code that reads a secret straight off the runtime environment — `environmentVariables['KEY']` or `environmentVariables.KEY` in JavaScript, `environmentVariables['KEY']` or `environmentVariables.get('KEY')` in Python, `$KEY` or `${KEY}` in shell — also activates masking, provided Sim can see the read in the code before it runs. A hardcoded literal never does: Sim has no way to know it came from a secret.
75
75
76
76
<Callouttype="warn">
77
-
Direct reads are found by reading the code, not by running it, so recognition stops where the code stops being readable ahead of time. Sim reports a direct read only when it can attribute one with certainty, and skips it otherwise — an unrecognized read is not masked, and does not appear under **See usage**.
77
+
Direct reads are found by reading the code, not by running it, so recognition stops where the code stops being readable ahead of time. An unrecognized read is not masked, and does not appear under **See usage**.
78
+
79
+
Where a read is recognized, Sim reports it rather than trying to prove it is not one. Code that shadows the environment binding with its own object, or overwrites a variable before reading it, is still reported: naming a secret costs only an exact value the code never emits, while failing to name one leaves it unmasked. **See usage** can therefore occasionally list a secret the code had available but did not read.
78
80
79
81
A read is **not** recognized when:
80
82
81
83
-**The name is built at runtime.**`environmentVariables[keyName]`, `$@`, `${!indirect}`, `eval`, `printenv`, or a sourced file hide which secret is being read.
82
-
-**The binding is reassigned.**If JavaScript or Python code declares its own `environmentVariables` — a variable, parameter, destructured binding, loop target, or assignment — reads off it are no longer the mounted environment, so Sim stops reporting direct reads in that file. In shell the same applies per variable: after `KEY=something`, `$KEY` is the script's own value, so that name is skipped while others are unaffected.
84
+
-**The read is of a different object.**`other.environmentVariables['KEY']`reads something that merely shares the name.
83
85
-**The read cannot be told apart from text.** A `$KEY` inside single quotes or a quoted heredoc (`<<'EOF'`) never expands, and Sim treats anything its scanner cannot place as not running.
84
86
85
87
Both masking and model-bound projection match only exact values in either case. Encoded, hashed, fragmented, or otherwise transformed versions are not matched, and a value assembled or emitted piece by piece cannot be matched at all — determining whether arbitrary code will eventually reveal a value is not decidable in general. Treat these as a safety net, not a boundary: do not deliberately return, print, or transmit secrets.
0 commit comments