// own-working-tree.test.mjs — isOwnWorkingTree() rule (v8.1.1). // // v8.1.0 treated any target at or below the process cwd as the caller's own // working tree. That is too wide: a foreign clone that sits UNDER cwd (cwd = // $HOME, or a project with a cloned vendor dir) still had its .llm-security // config read. v8.1.1 narrows the rule: the target must be at or below cwd // AND have the same git root as cwd — the nearest ancestor holding a `.git` // (directory for a clone, file for a submodule/worktree), or none for both. // A nested clone therefore counts as foreign. The failure direction is safe: // foreign means the target's config is ignored, so more findings, never fewer. // // Fixtures live under $HOME (outside os.tmpdir(), which is always foreign, // and outside this repo's git tree); $HOME itself must not be a git root. import { describe, it, before, after } from 'node:test'; import assert from 'node:assert/strict'; import { join } from 'node:path'; import { mkdirSync, writeFileSync, rmSync, existsSync } from 'node:fs'; import { homedir } from 'node:os'; import { spawnSync } from 'node:child_process'; import { isOwnWorkingTree } from '../../scanners/lib/own-working-tree.mjs'; import { mkOwnTreeDir } from '../helpers/own-tree.mjs'; function gitInit(dir) { mkdirSync(dir, { recursive: true }); const r = spawnSync('git', ['init', '-q', dir], { encoding: 'utf8' }); assert.equal(r.status, 0, `git init failed: ${r.stderr}`); } function ownFrom(cwd, target) { const prev = process.cwd(); process.chdir(cwd); try { return isOwnWorkingTree(target); } finally { process.chdir(prev); } } describe('isOwnWorkingTree(): same git root as cwd (v8.1.1)', () => { let root; let repo; let homeish; before(() => { assert.ok(!existsSync(join(homedir(), '.git')), 'precondition: $HOME is not a git root'); root = mkOwnTreeDir('owt-unit-'); // A repo the user works in, with a plain subdir, a nested clone and a // submodule-style checkout (`.git` is a file). repo = join(root, 'repo'); gitInit(repo); mkdirSync(join(repo, 'sub', 'deeper'), { recursive: true }); gitInit(join(repo, 'vendor', 'clone')); mkdirSync(join(repo, 'vendor', 'clone', 'inner'), { recursive: true }); mkdirSync(join(repo, 'submod'), { recursive: true }); writeFileSync(join(repo, 'submod', '.git'), 'gitdir: ../.git/modules/submod\n'); // A $HOME-like dir: no git root, holding one clone and one plain dir. homeish = join(root, 'homeish'); mkdirSync(join(homeish, 'plain'), { recursive: true }); gitInit(join(homeish, 'clone')); }); after(() => { rmSync(root, { recursive: true, force: true }); }); it('cwd itself is own', () => { assert.equal(ownFrom(repo, repo), true); assert.equal(ownFrom(repo, '.'), true); }); it('(b) a subdir of the same repo is own (known-positive)', () => { assert.equal(ownFrom(repo, join(repo, 'sub')), true); assert.equal(ownFrom(join(repo, 'sub'), join(repo, 'sub', 'deeper')), true); }); it('(a) a nested clone under the repo is foreign', () => { assert.equal(ownFrom(repo, join(repo, 'vendor', 'clone')), false); }); it('(a) a subdir inside a nested clone is foreign', () => { assert.equal(ownFrom(repo, join(repo, 'vendor', 'clone', 'inner')), false); }); it('a submodule/worktree checkout (`.git` file) is foreign', () => { assert.equal(ownFrom(repo, join(repo, 'submod')), false); }); it('(c) a clone under a cwd with no git root is foreign', () => { assert.equal(ownFrom(homeish, join(homeish, 'clone')), false); }); it('a plain subdir of a cwd with no git root is own', () => { assert.equal(ownFrom(homeish, join(homeish, 'plain')), true); }); it('a clone the user has cd\'d into is own (it is cwd)', () => { assert.equal(ownFrom(join(homeish, 'clone'), '.'), true); }); it('a target outside cwd is foreign', () => { assert.equal(ownFrom(join(repo, 'sub'), repo), false); }); }); // v8.1.2: a target with no `.git` of its own under cwd shares cwd's git root, // so the v8.1.1 rule alone calls it own. Two concrete places foreign code lands // under a user's working directory without a `.git`: an installed package // (a `node_modules` segment on the path from cwd to the target) and Claude // Code's plugin directory (`$CLAUDE_CONFIG_DIR/plugins`, default // `~/.claude/plugins` — cache/ and marketplaces/). Both are foreign now. describe('isOwnWorkingTree(): node_modules and the plugin dir are foreign (v8.1.2)', () => { let root; let repo; let savedConfigDir; let savedHome; before(() => { root = mkOwnTreeDir('owt-812-'); repo = join(root, 'repo'); gitInit(repo); mkdirSync(join(repo, 'node_modules', 'evil-pkg', 'lib'), { recursive: true }); mkdirSync(join(repo, 'node_modules', '@scope', 'pkg'), { recursive: true }); mkdirSync(join(repo, 'packages', 'app', 'node_modules', 'dep'), { recursive: true }); mkdirSync(join(repo, 'my-node_modules-notes'), { recursive: true }); // A config dir with a plugin-cache copy (no `.git`, like 234/235 real ones) // and a plugin config dir sitting in a plain (no git root) parent. mkdirSync(join(root, 'cfg', 'plugins', 'cache', 'mkt', 'evil', '1.0.0'), { recursive: true }); mkdirSync(join(root, 'cfg', 'plugins', 'marketplaces', 'mkt'), { recursive: true }); mkdirSync(join(root, 'cfg', 'projects'), { recursive: true }); mkdirSync(join(root, 'fakehome', '.claude', 'plugins', 'cache', 'mkt', 'p', '2.0.0'), { recursive: true }); savedConfigDir = process.env.CLAUDE_CONFIG_DIR; savedHome = process.env.HOME; }); after(() => { if (savedConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR; else process.env.CLAUDE_CONFIG_DIR = savedConfigDir; process.env.HOME = savedHome; rmSync(root, { recursive: true, force: true }); }); it('(a) an installed package under the repo is foreign', () => { assert.equal(ownFrom(repo, join(repo, 'node_modules', 'evil-pkg')), false); assert.equal(ownFrom(repo, join(repo, 'node_modules', 'evil-pkg', 'lib')), false); assert.equal(ownFrom(repo, join(repo, 'node_modules', '@scope', 'pkg')), false); assert.equal(ownFrom(repo, join(repo, 'node_modules')), false); }); it('(a) a nested node_modules (workspace package) is foreign', () => { assert.equal(ownFrom(repo, join(repo, 'packages', 'app', 'node_modules', 'dep')), false); }); it('(c) the workspace package itself and a look-alike name stay own (known-positive)', () => { assert.equal(ownFrom(repo, join(repo, 'packages', 'app')), true); assert.equal(ownFrom(repo, join(repo, 'my-node_modules-notes')), true); }); it('a package the user has cd\'d into is own (only the path from cwd counts)', () => { assert.equal(ownFrom(join(repo, 'node_modules', 'evil-pkg'), '.'), true); assert.equal(ownFrom(join(repo, 'node_modules', 'evil-pkg'), 'lib'), true); }); it('(b) a copy under $CLAUDE_CONFIG_DIR/plugins/cache is foreign', () => { process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg'); const target = join(root, 'cfg', 'plugins', 'cache', 'mkt', 'evil', '1.0.0'); assert.equal(ownFrom(root, target), false); assert.equal(ownFrom(join(root, 'cfg'), target), false); assert.equal(ownFrom(target, '.'), false); }); it('(b) a clone under $CLAUDE_CONFIG_DIR/plugins/marketplaces is foreign', () => { process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg'); assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'plugins', 'marketplaces', 'mkt')), false); }); it('(b) a relative CLAUDE_CONFIG_DIR is resolved against cwd', () => { process.env.CLAUDE_CONFIG_DIR = 'cfg'; assert.equal(ownFrom(root, join(root, 'cfg', 'plugins', 'cache', 'mkt', 'evil', '1.0.0')), false); }); it('(c) the rest of the config dir stays own (known-positive)', () => { process.env.CLAUDE_CONFIG_DIR = join(root, 'cfg'); assert.equal(ownFrom(join(root, 'cfg'), join(root, 'cfg', 'projects')), true); }); it('(b) without CLAUDE_CONFIG_DIR the default is ~/.claude/plugins', () => { delete process.env.CLAUDE_CONFIG_DIR; process.env.HOME = join(root, 'fakehome'); try { const home = join(root, 'fakehome'); assert.equal(ownFrom(home, join(home, '.claude', 'plugins', 'cache', 'mkt', 'p', '2.0.0')), false); assert.equal(ownFrom(home, join(home, '.claude')), true); } finally { process.env.HOME = savedHome; } }); });