fix(scope): node_modules and the Claude Code plugin dir are foreign
A target with no .git of its own under cwd shares cwd's git root, so the v8.1.1 rule alone called it own: an installed package under a repo and a plugin-cache copy still had their .llm-security-ignore / policy.json read. Chosen (PM rule, order 20260922T192716Z): a target is additionally foreign when the path from cwd to it has a node_modules segment, or when it lies under $CLAUDE_CONFIG_DIR/plugins (default ~/.claude/plugins). Because those are the two concrete places foreign code lands under a user's working directory, the failure direction is safe (foreign means more findings), and a general "no .git of its own" rule would shut out ordinary subdirectories of the caller's own repo. git archive exports stay indistinguishable from own subdirectories; documented as a known limit in the module header. Red first: 6 unit + 6 orchestrator assertions failed before the fix; known-positives (plain subdir, workspace package dir, rest of config dir) passed before and after. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
931466cffa
commit
a61c1c9648
3 changed files with 323 additions and 3 deletions
|
|
@ -100,3 +100,97 @@ describe('isOwnWorkingTree(): same git root as cwd (v8.1.1)', () => {
|
|||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue