Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
167a5f2
fix: mask filesystem paths in telemetry error text
Aug 19, 2026
4899189
fix: mask cloud-storage URIs and paths with embedded spaces
Aug 19, 2026
9d56682
fix: bracket anchors, spaced cloud keys, terminal spaced components
Aug 19, 2026
40cb7fe
fix: spaced-username tails and Windows closing delimiters
Aug 19, 2026
b466d17
fix: file:/ URIs, case-insensitive Windows homes, long-extension prose
Aug 19, 2026
12728e0
fix: unicode path components and colon-adjacent paths
Aug 19, 2026
acef14f
fix: combining marks, curly apostrophes, ;/< boundaries — and rule order
Aug 19, 2026
fabd76b
fix: ASCII apostrophes as path content when word-char follows
Aug 19, 2026
ab8417d
fix: delimiters inside path components — and the halting condition
Aug 19, 2026
7b794e7
fix: linearize the delimiter lookahead — ReDoS, and two coverage gaps
Aug 19, 2026
6e1d639
fix: dot-relative paths, symbol components, cloud terminal keys
Aug 19, 2026
a514316
refactor: compose path rules from shared fragments; five coverage fixes
Aug 19, 2026
8801300
fix: parens inside components, nested home roots (WSL)
Aug 19, 2026
f64496d
fix: windows dot-relative paths, attached dotted filenames after deli…
Aug 19, 2026
a880343
fix: backslash path components, extended-length windows homes; move h…
Aug 19, 2026
7d8678e
fix: double spaces in components, current-drive-rooted windows paths
Aug 19, 2026
be0daf9
fix: strip ANSI sequences, drive-relative windows paths, quote delimi…
Aug 19, 2026
f8939f1
fix: quote-pair safety, pipe anchors, OSC escape stripping
Aug 19, 2026
c963b71
fix: spaced first components in dot-relative paths, drive-relative fo…
Aug 19, 2026
499e956
fix: home-rule reach through spaced prefixes and UNC shares; drive-re…
Aug 19, 2026
7eb8382
fix: rooted-proof breadth (spaced/symbol components), tilde home tail
Aug 19, 2026
a5f79d8
fix: multi-word PII tails, proof punctuation, bounded delimiter scans
Aug 19, 2026
6a0dfd3
fix: proof bounds match the filesystem component limit; shallow path …
Aug 19, 2026
43b5d68
fix: spaced shallow terminals, name particles in PII tails, extension…
Aug 19, 2026
454cb28
fix: remove the i flag from home/cloud rules — case-fold breaks the t…
Aug 19, 2026
78f92a4
fix: legacy Documents and Settings home roots, 14-char extension window
Aug 19, 2026
adce061
fix: delimiter proofs accept mixed unspaced+spaced runs
Aug 19, 2026
96dbe0e
fix: shell redirects anchor paths, single-component absolute files mask
Aug 19, 2026
7565405
fix: closing delimiters anchor paths; forward-slash UNC with dotless …
Aug 19, 2026
b75d618
fix: colon-anchored bare absolute files (ENOENT:/private.sql)
Aug 19, 2026
7707270
fix: forward-slash UNC homes get the high-PII tail
Aug 19, 2026
af81386
fix: NBSP-bearing components, hyphenated extensions
Aug 19, 2026
1e1e2a3
fix: PowerShell backslash tilde paths
Aug 19, 2026
6bdbd1b
fix: dotfile tilde paths, letter-bearing drive proofs, NBSP particles
Aug 19, 2026
ddf1dad
fix: one-char backslash tilde components
Aug 19, 2026
0b43716
fix: one-char rooted components, brace anchors, unicode extensions
Aug 19, 2026
f902eea
fix: ampersand anchors, component-standard bounds, mark-bearing exten…
Aug 19, 2026
a811a23
fix: +& in rooted components (len 3+), 30-char extension window
Aug 19, 2026
2e1275f
fix: collapse pure separator runs before the composed rules
Aug 19, 2026
abd0ff4
feat: known-prefix literal masking layer, fast-path gates, shallow wi…
Aug 19, 2026
aa5d6d1
test: restructure path-masking suite by subject
Aug 19, 2026
ced57f4
fix: layer-zero ordering and boundaries, drive-relative single files
Aug 19, 2026
a32d716
fix: tilde symbol components, drive-relative terminal filename breadth
Aug 19, 2026
35320ed
fix: JSON-doubled home separators, honest NBSP tests, comment accuracy
Aug 19, 2026
22f0c54
chore: rewrap known-prefix comment to block style
Aug 19, 2026
4acad5e
fix: bounded bridges, linear drive-colon, fresh cwd, growth tests, FQ…
Aug 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 140 additions & 2 deletions packages/opencode/src/altimate/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const log = Log.create({ service: "telemetry" })
* | extend err = tostring(customDimensions.error_class)
* | summarize count() by err
*/

// altimate_change end

/** True when a test runner is driving the process rather than a real user session.
Expand All @@ -64,6 +65,91 @@ function isAutomatedRun(): boolean {
return Boolean(process.env.BUN_TEST || process.env.VITEST || process.env.JEST_WORKER_ID)
}

// altimate_change start — composed path-masking rules (shared fragments)
// R: one path-run character — anything but whitespace/separators/quotes,
// plus an apostrophe when a word character follows (O'Connor vs closing ').
const PM_R = "(?:[^\\s\\/\\\\'\"`]|'(?=[\\p{L}\\p{N}_]))"
// slash-delimited variant: backslash is path content, not a separator
const PM_R_P = "(?:[^\\s\\/'\"`]|'(?=[\\p{L}\\p{N}_]))"
const PM_WORD = "(?:[\\p{L}\\p{M}\\p{N}_‘’-]|'(?=[\\p{L}\\p{N}_]))"
const PM_ANCHOR = "(^|[\\s\"'`=(,[{:;<|>)\\]}&])"
const PM_SP = "[^\\S\\t\\n\\r\\v\\f]"
const PM_EXT = "\\.[\\p{L}\\p{M}\\p{N}-]{0,29}[\\p{L}\\p{M}\\p{N}]"
// span char: path content incl. delimiters (, ; ) ] } >) that a later
// separator — or an attached dotted terminal filename (;draft.sql) —
// proves is path content — multi-word spaced runs allowed, all
// quantified units space- or separator-anchored with disjoint inner classes
// (unambiguous parse => linear time; the nested-quantifier ReDoS shape is
// banned here).
const SEP_P = "\\/"
const SEP_W = "[\\\\\\/]"
// per-letter case expansion — used instead of the i flag on home/cloud
// rules: under /iu, conformant engines case-fold \p{Lu}, which would turn
// the capitalized-tail gate into "match any word" (V8 folds; JSC does not —
// never rely on the divergence)
const pmCI = (w: string) => w.split("").map((c) => ("[" + c + c.toUpperCase() + "]")).join("")
const pmR = (sep: string) => (sep === SEP_P ? PM_R_P : PM_R)
const PM_WR = "(?:[^\\s\\/\\\\'\"`,;:\\]}>]|'(?=[\\p{L}\\p{N}_]))"
const PM_WR_P = "(?:[^\\s\\/'\"`,;:\\]}>]|'(?=[\\p{L}\\p{N}_]))"
const pmWR = (sep: string) => (sep === SEP_P ? PM_WR_P : PM_WR)
const PM_SEG_L = "(?=[^\\s'\"`]{0,128}[\\p{L}\\p{M}]|[\\s'\"`,;)\\]}>]|$)"
const pmSpan = (sep: string) =>
"(?:[^\\s'\"`)\\]},;>]|'(?=[\\p{L}\\p{N}_])|[,;)\\]}>](?=" + pmR(sep) + "{0,256}(?:" + PM_SP + "{1,2}" + pmWR(sep) + "{1,64}){0,2}(?:" + sep + PM_SEG_L + "|" + PM_EXT + "(?=$|[\\s.,;:)\\]}!?])))|[\"'`](?=" + pmR(sep) + "{1,256}(?:" + PM_SP + "{1,2}" + pmWR(sep) + "{1,64}){0,2}(?:" + sep + PM_SEG_L + "|" + PM_EXT + "(?=$|[\\s.,;:)\\]}!?]))))"
const pmChunks = (sep: string) =>
"(?:(?:" + PM_SP + "{1,2}" + pmWR(sep) + "{1,64}){1,2}" + sep + PM_SEG_L + pmSpan(sep) + "*){0,2}"
// terminal dotted filename: up to four spaced words that END in an extension
const pmSpFile = (sep: string) => "(?:(?:" + PM_SP + "{1,2}" + pmR(sep) + "+){1,4}(?<=" + PM_EXT + "))?"
Comment on lines +100 to +101

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mask terminal filenames with more than four spaced words

When an explicit shallow path has five or more spaced continuations, for example read ./Q1 final audited customer revenue report.sql failed, the bounded repetition rejects the terminal filename and the entire customer-specific path remains unchanged. Filesystems do not impose a four-word limit, so keep the match linear using a component-length bound rather than limiting the number of words.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f902eea91 — with an important scope line: explicit shallow paths (./-prefixed, where everything up to the boundary is filename) now use a length-bounded variant (12 words × 64 chars), so your example masks. Deep tails keep {1,4} deliberately — there the bound is a prose cap, not a filesystem claim: read /opt/x error reading the project config.yml here must not chase the dotted token through five prose words (this exact trade-off was probed and documented in the earlier spfile thread; the control is re-pinned alongside your fix).

const PM_TERM_COND = "(?:(?<!" + PM_EXT + ")" + PM_SP + "{1,2}" + PM_WORD + "+(?=$|[.,;:)\\]}!?]))?"
const PM_TERM_UNC =
"(?:(?<!" + PM_EXT + ")" + PM_SP + "{1,2}" + PM_WORD + "+(?:" + PM_SP + "{1,2}(?:[\\p{Lu}\\p{Lo}]" + PM_WORD + "*|(?:v[ao]n|de[nrl]?|d[aiou]|dos|la|les?|los|bin|ibn|al|el|te[nr])(?=" + PM_SP + ")))*)?"
const PM_WC = "(?:[\\p{L}\\p{N}_#@().'-]{2,}|[\\p{L}\\p{N}_#@().'+&-]{3,})"
const PM_WCC = "[\\p{L}\\p{N}_#@().'+&-]+"
const pmSpFileX = (sep: string) => "(?:(?:" + PM_SP + "{1,2}" + pmR(sep) + "{1,64}){1,12}(?<=" + PM_EXT + "))?"
const pmTail = (sep: string, term: string) => pmSpan(sep) + "*" + pmChunks(sep) + pmSpFile(sep) + term
// Known-prefix literals — the local user's home and cwd are KNOWN values,
// replaced by exact match AFTER the structural rules (structure must see the
// original string: stripping the prefix first orphans terminal spaced
// components). The literal pass mops up whatever structure missed. Exact
// matching handles every username shape (spaces, NBSP, unicode) with zero
// false positives; the structural rules below remain for paths the
// literals cannot know (other drives, UNC shares, cloud URIs, relative
// forms, WSL-mounted homes). Variants cover JSON-doubled backslashes and
// swapped separators. Same approach as Salesforce's telemetry GDPR scrub
// (os.homedir() literal) and gatsby-telemetry's cleanPaths (cwd prefixes).
const pmEscape = (v: string) => v.replace(/[-[\]/{}()*+?.\\^$|]/g, "\\$&")
const pmPrefixVariants = (root: string): RegExp[] => {
if (!root || root.length < 4) return []
const out: RegExp[] = []
for (const v of new Set([root, root.replace(/\\/g, "\\\\"), root.replace(/\\/g, "/")])) {
out.push(new RegExp("(?<![\\w.-])" + pmEscape(v), "gi"))
}
return out
}
const PM_HOME_PREFIXES = pmPrefixVariants(os.homedir())
// The CLI chdirs into the project after this module loads (tui/attach/run),
// so the cwd literals are rebuilt whenever process.cwd() changes — a stale
// import-time cwd would miss exactly the shallow, extensionless project root
// the structural rules cannot mask.
let pmCwdCache = ""
let pmCwdPrefixes: RegExp[] = []
function pmKnownPrefixes(): RegExp[] {
const cwd = process.cwd()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When the process cwd has been deleted or becomes inaccessible, pmKnownPrefixes() throws from process.cwd() while masking an error. Catch that lookup failure and retain the last cached cwd (or skip cwd prefixes) so telemetry masking cannot turn the original error into a second exception.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/telemetry/index.ts, line 136:

<comment>When the process cwd has been deleted or becomes inaccessible, `pmKnownPrefixes()` throws from `process.cwd()` while masking an error. Catch that lookup failure and retain the last cached cwd (or skip cwd prefixes) so telemetry masking cannot turn the original error into a second exception.</comment>

<file context>
@@ -112,19 +117,35 @@ const pmTail = (sep: string, term: string) => pmSpan(sep) + "*" + pmChunks(sep)
+let pmCwdCache = ""
+let pmCwdPrefixes: RegExp[] = []
+function pmKnownPrefixes(): RegExp[] {
+  const cwd = process.cwd()
+  if (cwd !== pmCwdCache) {
+    pmCwdCache = cwd
</file context>
Suggested change
const cwd = process.cwd()
const cwd = (() => {
try {
return process.cwd()
} catch {
return pmCwdCache
}
})()

if (cwd !== pmCwdCache) {
pmCwdCache = cwd
pmCwdPrefixes = pmPrefixVariants(cwd)
}
return [...pmCwdPrefixes, ...PM_HOME_PREFIXES]
}
const PATH_RULES = {
cloud: new RegExp(PM_ANCHOR + "(?:(?:" + [pmCI("gs"), pmCI("s3") + "[anAN]?", pmCI("abfs") + "[sS]?", pmCI("wasb") + "[sS]?", pmCI("adl"), pmCI("dbfs"), pmCI("hdfs")].join("|") + "):\\/\\/|" + pmCI("file") + ":\\/{1,3})" + pmTail(SEP_P, PM_TERM_UNC), "gu"),
windowsHome: new RegExp(PM_ANCHOR + "(?:(?:\\\\\\\\\\?\\\\)?[A-Za-z]:" + SEP_W + "{0,2}|(?:\\\\\\\\(?:\\?\\\\" + pmCI("unc") + "\\\\)?|(?<!:)\\/\\/(?=[^\\s\\/\\\\]+" + SEP_W + "))(?:" + PM_R + "+(?:" + PM_SP + "{1,2}" + PM_WR + "{1,64})*" + SEP_W + "{1,2})*|" + SEP_W + "{1,2})(?:" + pmCI("users") + "|" + pmCI("documents") + " " + pmCI("and") + " " + pmCI("settings") + ")" + SEP_W + "{1,2}" + pmTail(SEP_W, PM_TERM_UNC), "gu"),
windows: new RegExp(PM_ANCHOR + "(?:[A-Za-z]:" + SEP_W + "|(?<!:)\\/\\/(?=[^\\s\\/\\\\.]+" + SEP_W + ")|[A-Za-z]:(?=" + PM_R + "{1,256}(?:" + PM_SP + "{1,2}" + PM_R + "{1,256})*\\\\|(?=[^\\s\\/\\\\]{0,256}[\\p{L}])" + PM_R + "{1,256}(?:" + PM_SP + "{1,2}" + PM_R + "{1,256})*\\/|" + PM_R + "{1,256}(?:" + PM_SP + "{1,2}" + PM_R + "{1,256}){0,6}(?<=\\.[\\p{L}\\p{M}\\p{N}-]{0,29})(?<=[\\p{L}\\p{M}][\\p{L}\\p{M}\\p{N}-]{0,29})(?=$|[\\s.,;:)\\]}!?]))|\\\\\\\\|\\.{1,2}\\\\(?=" + PM_R + "{1,256}(?:" + PM_SP + "{1,2}" + PM_R + "{1,256})*\\\\|[^\\s\\\\]{1,256}" + PM_EXT + "(?=$|[\\s.,;:)\\]}!?]))|\\\\(?=(?:" + PM_WC + "(?:" + PM_SP + "{1,2}" + PM_WCC + ")*\\\\){2}|" + PM_WC + "(?:" + PM_SP + "{1,2}" + PM_WCC + ")*\\\\[^\\s\\\\]{1,256}" + PM_EXT + "(?=$|[\\s.,;:)\\]}!?])|" + PM_WC + "(?:" + PM_SP + "{1,2}" + PM_WCC + ")+\\\\[\\p{L}\\p{N}]|[\\p{L}\\p{N}_-]\\\\(?:[\\p{L}\\p{N}_-]{2,}|\\.[\\p{L}\\p{N}_-]{2,})|[^\\s\\\\]{1,256}" + PM_EXT + "(?=$|[\\s.,;:)\\]}!?])))" + pmSpan(SEP_W) + "+" + pmChunks(SEP_W) + pmSpFile(SEP_W) + PM_TERM_COND, "gu"),
posixHome: new RegExp(PM_ANCHOR + "\\/(?:" + PM_R_P + "+(?:" + PM_SP + "{1,2}" + PM_WR_P + "{1,64}){0,2}\\/" + PM_SEG_L + ")*(?:" + pmCI("users") + "|" + pmCI("home") + ")\\/" + pmTail(SEP_P, PM_TERM_UNC), "gu"),
posix: new RegExp(PM_ANCHOR + "(?:\\.{0,2}\\/(?:" + PM_R_P + "+(?:" + PM_SP + "{1,2}" + PM_WR_P + "{1,64}){0,2}\\/" + PM_SEG_L + ")+" + pmTail(SEP_P, PM_TERM_COND) + "|(?:\\.{1,2}\\/|\\/(?!\\/))" + pmSpan(SEP_P) + "+" + pmSpFileX(SEP_P) + "(?<=" + PM_EXT + ")(?=$|[\\s.,;:)\\]}!?]))", "gu"),
tilde: new RegExp(PM_ANCHOR + "~[\\p{L}\\p{M}\\p{N}_.-]*(?:\\/|\\\\(?=" + PM_WC + "(?:" + PM_SP + "{1,2}" + PM_WCC + ")*[\\\\\\/]|\\.[\\p{L}\\p{N}_-]{2,}[\\\\\\/]|[\\p{L}\\p{N}_-]\\\\(?:[\\p{L}\\p{N}_-]{2,}|\\.[\\p{L}\\p{N}_-]{2,})))" + pmTail(SEP_W, PM_TERM_UNC), "gu"),
}
// altimate_change end

export namespace Telemetry {
const FLUSH_INTERVAL_MS = 5_000
const MAX_BUFFER_SIZE = 200
Expand Down Expand Up @@ -1381,12 +1467,64 @@ export namespace Telemetry {
// Bearer … Authorization headers leaked in error text
// Each match replaces with a fixed redaction so length-based fingerprinting
// can't reconstruct the original token.

export function maskString(s: string): string {
return s
// Consumers truncate masked output to <= 2000 chars; masking beyond 8 KB
// of input buys nothing and unbounded input is what turns any super-
// linear rule into a stall (a wide generated SELECT reached seconds).
if (s.length > 8192) s = s.slice(0, 8192)
let out = s
// ANSI CSI sequences (colored subprocess stderr) would otherwise split
// tokens so neither credential nor path rules can see them
.replace(/\x1b(?:\[[0-?]*[ -\/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/g, "")
.replace(/sk-(?:ant-)?[A-Za-z0-9_-]{20,}/g, "sk-***")
.replace(/Bearer\s+[A-Za-z0-9._-]{20,}/gi, "Bearer ***")
// Fast path: a string with no separator cannot contain a path — skip the
// whole path stack (most telemetry strings carry no path at all).
if (out.includes("/") || out.includes("\\") || /(?<![A-Za-z0-9])[A-Za-z]:[^\s:]{1,255}(?: [^\s:]{1,255}){0,2}\.[A-Za-z]/.test(out)) {
out = out
// altimate_change start — mask filesystem paths in error text
// Six masking rules (cloud URIs, Windows home, Windows/UNC incl. .\ and
// ..\, POSIX home, POSIX incl. ./ and ../, ~ incl. ~username), composed from shared
// fragments below (PATH_RULES) — one source of truth after repeated
// lockstep edits drifted (see PR history). Ordered after the credential
// rules and BEFORE the email/internal-host rules so whole URIs mask
// before userinfo can fragment into <email>. Public URL interiors are
// structurally safe: after "https:" comes "//", which cannot start a
// segment chain. Doctrine: over-masking is the correct failure mode.
// HOME-ROOTED paths and CLOUD URIs consume one unconditional trailing
// word (spaced usernames / object keys — the high-PII classes),
// suppressed after a dotted extension so "x.sql was deleted" prose
// survives; other rules consume a trailing word only at end-of-string /
Comment thread
ralphstodomingo marked this conversation as resolved.
// before punctuation — except spaced terminal FILENAMES, which may span
// interior words when the run ends in a dotted extension (up to 4 words
// on deep paths, 12 on explicit shallow ./-style paths). Residue (by design): one prose word may be
// over-masked after extensionless home/cloud paths; a non-home,
// non-cloud path's terminal spaced component can leak ONE structure
// word mid-sentence (no personal names in that class); a delimiter
// followed by neither a further separator nor a dotted terminal
// filename is a permanent boundary.
.replace(/(^|[\s"'`=(,[{:;<|>)\]}&])[\\/]{4,}(?=$|[\s"'`,;)\]}<>|&])/g, "$1<path>")
.replace(PATH_RULES.cloud, "$1<path>")
// the windows rules carry the widest opener alternation — they cannot
// match without a backslash, a boundary drive-colon, or a non-scheme //
if (out.includes("\\") || /(?<![A-Za-z0-9])[A-Za-z]:/.test(out) || /(?<!:)\/\//.test(out)) {
out = out.replace(PATH_RULES.windowsHome, "$1<path>").replace(PATH_RULES.windows, "$1<path>")
}
out = out
.replace(PATH_RULES.posixHome, "$1<path>")
.replace(PATH_RULES.posix, "$1<path>")
.replace(PATH_RULES.tilde, "$1<path>")
for (const re of pmKnownPrefixes()) out = out.replace(re, "<path>")
out = out
// a literal-prefix mask followed by a structurally-masked remainder
// collapses to one marker
.replace(/<path>(?:[\\/]?<path>)+/g, "<path>")
}
return out
// altimate_change end
// Email addresses — providers occasionally echo caller identity in error text.
.replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "<email>")
.replace(/(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "<email>")
// Internal hostnames in URLs — keeps parity with `parseAPICallError`'s
// `maskInternalHost` so an error message containing the same URL doesn't
// leak through telemetry while metadata.url is masked. Covers:
Expand Down
Loading
Loading