fix(scope): venv, vendor/, config skills/, both config dirs, case-canonical paths
PLAN § v8.1.3 punkt 1-3, PM decisions in order 20260923T092223Z: - punkt 1: a `site-packages` or `vendor` segment below cwd is foreign like `node_modules`; a config dir's `skills/` is foreign like its `plugins/`. Safe direction: a false foreign costs an extra finding. - punkt 2: `~/.claude` and `$CLAUDE_CONFIG_DIR` both count; a leading `~` in the variable is expanded. Anthropic's docs (claude-directory, env-vars) do not say whether Claude Code expands it: not verified, so both readings are foreign. - punkt 3: realpathSync.native canonicalizes case on a case-insensitive volume (measured: realpathSync keeps `sub`, .native gives `Sub`), so NODE_MODULES/x is foreign and a case-mismatched own path is own (closes § v8.1.2 punkt 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b62c3e60f5
commit
002c0bac99
1 changed files with 49 additions and 21 deletions
|
|
@ -28,10 +28,22 @@
|
||||||
// target under Claude Code's plugin dir — `$CLAUDE_CONFIG_DIR/plugins`, default
|
// target under Claude Code's plugin dir — `$CLAUDE_CONFIG_DIR/plugins`, default
|
||||||
// `~/.claude/plugins` (cache/ and marketplaces/). A general "no `.git` of its
|
// `~/.claude/plugins` (cache/ and marketplaces/). A general "no `.git` of its
|
||||||
// own" rule was not taken: it would shut out ordinary subdirectories of the
|
// own" rule was not taken: it would shut out ordinary subdirectories of the
|
||||||
// caller's own repo. Known limits — these still count as own: a `git archive`
|
// caller's own repo. Known limit in v8.1.2: other install locations still
|
||||||
// export or unpacked tarball under cwd (no marker at all), and other install
|
// counted as own.
|
||||||
// locations this rule does not name (a Python venv's site-packages, a
|
//
|
||||||
// composer/bundler `vendor/` dir, skills copied into a git-tracked ~/.claude).
|
// v8.1.3 (order 20260923T092223Z, PM decisions) widened both checks:
|
||||||
|
// (1) a `site-packages` (Python venv) or `vendor` (composer/bundler/Go)
|
||||||
|
// segment below cwd is foreign like `node_modules`, and a Claude Code config
|
||||||
|
// dir's `skills/` is foreign like its `plugins/` (third-party skills copied
|
||||||
|
// into a git-tracked ~/.claude share its git root); (2) the config dirs are
|
||||||
|
// BOTH `~/.claude` and `$CLAUDE_CONFIG_DIR`, whose leading `~` is expanded
|
||||||
|
// (Anthropic's docs do not say whether Claude Code expands it — not verified,
|
||||||
|
// so both readings count as foreign); (3) paths are realpath'd with
|
||||||
|
// `realpathSync.native`, which canonicalizes case on a case-insensitive
|
||||||
|
// volume: `NODE_MODULES/x` is the `node_modules` it names, and a
|
||||||
|
// case-mismatched path to an own dir is own. Still own (known limits): a
|
||||||
|
// `git archive` export or unpacked tarball under cwd (no marker at all), and
|
||||||
|
// cwd inside `node_modules` with a sibling package as target.
|
||||||
|
|
||||||
import { resolve, sep, join, dirname, relative } from 'node:path';
|
import { resolve, sep, join, dirname, relative } from 'node:path';
|
||||||
import { realpathSync, existsSync } from 'node:fs';
|
import { realpathSync, existsSync } from 'node:fs';
|
||||||
|
|
@ -52,21 +64,37 @@ function gitRoot(start) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Path segments below cwd that mark installed third-party code.
|
||||||
* Claude Code's plugin dir: `$CLAUDE_CONFIG_DIR/plugins` (a relative value is
|
const INSTALL_SEGMENTS = ['node_modules', 'site-packages', 'vendor'];
|
||||||
* resolved against cwd), else `~/.claude/plugins`. Realpath'd when it exists.
|
|
||||||
* @returns {string}
|
// Subdirs of a Claude Code config dir that hold third-party code.
|
||||||
*/
|
const CONFIG_INSTALL_DIRS = ['plugins', 'skills'];
|
||||||
function pluginDir() {
|
|
||||||
const configDir = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
/** Realpath with case canonicalized; the input itself when it does not exist. */
|
||||||
const dir = resolve(configDir, 'plugins');
|
function realOrSelf(p) {
|
||||||
try {
|
try {
|
||||||
return realpathSync(dir);
|
return realpathSync.native(p);
|
||||||
} catch {
|
} catch {
|
||||||
return dir;
|
return p;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude Code's install dirs: `plugins/` and `skills/` under `~/.claude` and,
|
||||||
|
* when set, under `$CLAUDE_CONFIG_DIR` (a leading `~` is expanded, a relative
|
||||||
|
* value is resolved against cwd).
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function configInstallDirs() {
|
||||||
|
const configDirs = [join(homedir(), '.claude')];
|
||||||
|
const env = process.env.CLAUDE_CONFIG_DIR;
|
||||||
|
if (env) {
|
||||||
|
const expanded = env === '~' || env.startsWith('~/') ? join(homedir(), env.slice(1)) : env;
|
||||||
|
configDirs.push(resolve(expanded));
|
||||||
|
}
|
||||||
|
return configDirs.flatMap(dir => CONFIG_INSTALL_DIRS.map(sub => realOrSelf(join(dir, sub))));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} targetPath
|
* @param {string} targetPath
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
|
|
@ -76,21 +104,21 @@ export function isOwnWorkingTree(targetPath) {
|
||||||
let resolvedCwd;
|
let resolvedCwd;
|
||||||
let resolvedTmp;
|
let resolvedTmp;
|
||||||
try {
|
try {
|
||||||
resolvedTarget = realpathSync(resolve(targetPath));
|
resolvedTarget = realpathSync.native(resolve(targetPath));
|
||||||
resolvedCwd = realpathSync(process.cwd());
|
resolvedCwd = realpathSync.native(process.cwd());
|
||||||
resolvedTmp = realpathSync(tmpdir());
|
resolvedTmp = realpathSync.native(tmpdir());
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (resolvedTarget === resolvedTmp || resolvedTarget.startsWith(resolvedTmp + sep)) {
|
if (resolvedTarget === resolvedTmp || resolvedTarget.startsWith(resolvedTmp + sep)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const plugins = pluginDir();
|
for (const dir of configInstallDirs()) {
|
||||||
if (resolvedTarget === plugins || resolvedTarget.startsWith(plugins + sep)) {
|
if (resolvedTarget === dir || resolvedTarget.startsWith(dir + sep)) return false;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
const underCwd = resolvedTarget === resolvedCwd || resolvedTarget.startsWith(resolvedCwd + sep);
|
const underCwd = resolvedTarget === resolvedCwd || resolvedTarget.startsWith(resolvedCwd + sep);
|
||||||
if (!underCwd) return false;
|
if (!underCwd) return false;
|
||||||
if (relative(resolvedCwd, resolvedTarget).split(sep).includes('node_modules')) return false;
|
const below = relative(resolvedCwd, resolvedTarget).split(sep);
|
||||||
|
if (below.some(segment => INSTALL_SEGMENTS.includes(segment))) return false;
|
||||||
return gitRoot(resolvedTarget) === gitRoot(resolvedCwd);
|
return gitRoot(resolvedTarget) === gitRoot(resolvedCwd);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue