mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
refactor: share the containment gate with hook-before-edit
hook-before-edit.mjs kept its own string-based isInsideProject; it now uses the shared isScanTargetInsideProject so all three hook passes apply one containment semantic, symlink canonicalization included. Because the before-edit hook gates proposed Writes whose target does not exist yet, canonicalPath now resolves the nearest existing ancestor and re-appends the remainder instead of falling back to the raw resolved path — a new file under a symlinked root compares equal to its canonical project. Written with AI assistance (Claude Code). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Code
parent
ae03e9e09c
commit
febce52e8d
@@ -23,6 +23,7 @@ import {
|
||||
designSystemOptions,
|
||||
filterFindings,
|
||||
isNativePlatform,
|
||||
isScanTargetInsideProject,
|
||||
loadDetector,
|
||||
matchConfiguredExtension,
|
||||
matchesAnyGlob,
|
||||
@@ -161,7 +162,7 @@ function replaceOnce(original, oldString, newString) {
|
||||
}
|
||||
|
||||
function readExistingProjectFile(filePath, cwd) {
|
||||
if (!isInsideProject(filePath, cwd)) return null;
|
||||
if (!isScanTargetInsideProject(filePath, cwd)) return null;
|
||||
if (SENSITIVE_PATH.test(filePath) || GENERATED_PATH.test(filePath)) return null;
|
||||
try {
|
||||
const stat = fs.statSync(filePath);
|
||||
@@ -232,7 +233,7 @@ function shellCopiedFileContent(command, cwd) {
|
||||
const source = shellCopyPaths(command)?.source;
|
||||
if (!source) return '';
|
||||
const sourcePath = path.isAbsolute(source) ? source : path.resolve(cwd, source);
|
||||
if (!isInsideProject(sourcePath, cwd)) return '';
|
||||
if (!isScanTargetInsideProject(sourcePath, cwd)) return '';
|
||||
if (SENSITIVE_PATH.test(sourcePath) || GENERATED_PATH.test(sourcePath)) return '';
|
||||
try {
|
||||
const stat = fs.statSync(sourcePath);
|
||||
@@ -328,15 +329,6 @@ function relativePath(filePath, cwd) {
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideProject(filePath, cwd) {
|
||||
try {
|
||||
const rel = path.relative(cwd, filePath);
|
||||
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// The static HTML engine reads its input from disk, but preToolUse only has
|
||||
// the proposed content. Stage it in a temp file so html-engine targets get the
|
||||
// same DOM-structural rules pre-write that runHook applies post-edit.
|
||||
@@ -414,7 +406,7 @@ async function main() {
|
||||
};
|
||||
|
||||
if (!filePath) return allow({ ...audit, skipped: 'no-file-path', durationMs: Date.now() - started });
|
||||
if (!isInsideProject(filePath, cwd)) return allow({ ...audit, skipped: 'outside-project', durationMs: Date.now() - started });
|
||||
if (!isScanTargetInsideProject(filePath, cwd)) return allow({ ...audit, skipped: 'outside-project', durationMs: Date.now() - started });
|
||||
if (SENSITIVE_PATH.test(filePath)) return allow({ ...audit, skipped: 'sensitive', durationMs: Date.now() - started });
|
||||
if (GENERATED_PATH.test(filePath)) return allow({ ...audit, skipped: 'generated', durationMs: Date.now() - started });
|
||||
|
||||
|
||||
+24
-10
@@ -1332,19 +1332,33 @@ function isInsideProject(filePath, projectCwd) {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve a path to its canonical (symlink-free) form. When the path does
|
||||
// not exist yet — the before-edit hook gates proposed Writes — canonicalize
|
||||
// the nearest existing ancestor and re-append the remainder, so a new file
|
||||
// under a symlinked root still compares equal to its canonical project.
|
||||
function canonicalPath(p) {
|
||||
try { return fs.realpathSync(p); } catch { return path.resolve(p); }
|
||||
const resolved = path.resolve(p);
|
||||
let dir = resolved;
|
||||
const tail = [];
|
||||
while (true) {
|
||||
try {
|
||||
return tail.length ? path.join(fs.realpathSync(dir), ...tail) : fs.realpathSync(dir);
|
||||
} catch { /* keep climbing */ }
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) return resolved;
|
||||
tail.unshift(path.basename(dir));
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
// Containment gate for both scan passes. A session routinely touches files
|
||||
// that belong to no project or to a different one — harness scratchpad dirs
|
||||
// under the system temp root, sibling checkouts, one-off throwaway HTML — and
|
||||
// findings against those are judged with THIS project's config and DESIGN.md
|
||||
// palette, which is never right. Skip them (audit reason: outside-project).
|
||||
// Paths are canonicalized first so a symlinked root (macOS /tmp ->
|
||||
// /private/tmp) doesn't split the comparison; the realpath fallback for
|
||||
// missing paths is only correct because both scan loops check existence
|
||||
// before calling this.
|
||||
// Containment gate shared by the before-edit hook and both scan passes. A
|
||||
// session routinely touches files that belong to no project or to a
|
||||
// different one — harness scratchpad dirs under the system temp root,
|
||||
// sibling checkouts, one-off throwaway HTML — and findings against those are
|
||||
// judged with THIS project's config and DESIGN.md palette, which is never
|
||||
// right. Skip them (audit reason: outside-project). Paths are canonicalized
|
||||
// first so a symlinked root (macOS /tmp -> /private/tmp) doesn't split the
|
||||
// comparison.
|
||||
export function isScanTargetInsideProject(filePath, projectCwd) {
|
||||
if (!filePath || !projectCwd) return false;
|
||||
return isInsideProject(canonicalPath(filePath), canonicalPath(projectCwd));
|
||||
|
||||
@@ -195,6 +195,19 @@ describe('isScanTargetInsideProject()', () => {
|
||||
assert.equal(isScanTargetInsideProject(file, link), true);
|
||||
assert.equal(isScanTargetInsideProject(path.join(link, 'src', 'Card.tsx'), real), true);
|
||||
});
|
||||
|
||||
it('classifies not-yet-written files by their nearest existing ancestor', () => {
|
||||
// The before-edit hook gates proposed Writes, so the target often does
|
||||
// not exist. Canonicalization must climb to an existing ancestor rather
|
||||
// than bail, or a new file under a symlinked root would read as outside.
|
||||
const real = path.join(root, 'real');
|
||||
const link = path.join(root, 'link');
|
||||
fs.mkdirSync(real, { recursive: true });
|
||||
fs.symlinkSync(real, link);
|
||||
assert.equal(isScanTargetInsideProject(path.join(link, 'src', 'New.tsx'), real), true);
|
||||
assert.equal(isScanTargetInsideProject(path.join(real, 'deep', 'New.tsx'), link), true);
|
||||
assert.equal(isScanTargetInsideProject(path.join(root, 'elsewhere', 'New.tsx'), real), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readConfig()', () => {
|
||||
|
||||
Reference in New Issue
Block a user