diff --git a/tests/path-replacement.test.cjs b/tests/path-replacement.test.cjs index 4c0bebc62..95eb2622e 100644 --- a/tests/path-replacement.test.cjs +++ b/tests/path-replacement.test.cjs @@ -30,6 +30,17 @@ function computePathPrefix(homedir, targetDir) { return resolvedTarget + '/'; } +// Detect whether `content` leaks a resolved absolute homedir path (e.g. +// /home/alice or /root). A bare substring match false-positives when homedir +// is short and happens to appear inside ordinary words or tags — for example +// `` when os.homedir() === '/root' (Docker). Real path +// leaks are followed by a path separator, so we require a trailing '/'. +// See #3503. +function containsResolvedHomedir(content, normalizedHomedir) { + if (!normalizedHomedir || normalizedHomedir === '$HOME') return false; + return content.includes(normalizedHomedir + '/'); +} + describe('pathPrefix computation', () => { test('default Claude global install uses $HOME/', () => { const homedir = os.homedir(); @@ -160,10 +171,34 @@ describe('installed .md files contain no resolved absolute paths', () => { let content = fs.readFileSync(file, 'utf8'); content = content.replace(claudeDirRegex, pathPrefix); content = content.replace(claudeHomeRegex, pathPrefix); - if (content.includes(normalizedHomedir) && normalizedHomedir !== '$HOME') { + if (containsResolvedHomedir(content, normalizedHomedir)) { failures.push(path.relative(repoRoot, file)); } } assert.deepStrictEqual(failures, [], `Files with resolved absolute paths: ${failures.join(', ')}`); }); }); + +describe('containsResolvedHomedir predicate (#3503)', () => { + test('flags a real homedir path leak with trailing slash', () => { + const content = 'see /home/alice/.claude/config for details'; + assert.strictEqual(containsResolvedHomedir(content, '/home/alice'), true); + }); + + test('does NOT flag short homedir appearing as substring of an identifier (#3503)', () => { + // Regression: in Docker, os.homedir() === '/root'. Agent markdown contains + // `` / `` tags. The old naive + // substring check false-fired on these. The trailing-slash rule fixes it. + const content = '\nfoo\n'; + assert.strictEqual(containsResolvedHomedir(content, '/root'), false); + }); + + test('still flags /root when followed by a real path separator', () => { + const content = 'cat /root/.claude/agents.md'; + assert.strictEqual(containsResolvedHomedir(content, '/root'), true); + }); + + test('returns false for $HOME placeholder', () => { + assert.strictEqual(containsResolvedHomedir('$HOME/.claude/', '$HOME'), false); + }); +});