MAESTRO: Prevent nested duplicate directory creation in CLAUDE.md paths (PR #836 concept)
Add hasConsecutiveDuplicateSegments() check to isValidPathForClaudeMd() to reject paths like frontend/frontend/ or src/src/ that occur when cwd already includes the directory name. 3 new tests added (46 total for claude-md-utils). Fixes #814. Credit to @Glucksberg. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,7 +25,8 @@ These all modify `src/utils/claude-md-utils.ts` — review together.
|
||||
- [x] Review PR #974 (`fix: prevent race condition when editing CLAUDE.md (#859)` by @cheapsteak). Files: `src/utils/claude-md-utils.ts`, tests. Race condition where concurrent edits corrupt CLAUDE.md. Steps: (1) `gh pr checkout 974` (2) Review locking/atomic write approach (3) Check test coverage (4) Run `npm run build` (5) If clean: `gh pr merge 974 --rebase --delete-branch`
|
||||
- **CLOSED — FIX APPLIED ON MAIN** (2026-02-05): PR had merge conflicts on built files (plugin/scripts/*.cjs) but source changes were clean and well-designed. Applied the exact approach to main: two-pass detection where first pass identifies folders containing CLAUDE.md files that appear in the observation's file paths, second pass skips those folders during CLAUDE.md updates. This prevents "file modified since read" errors when Claude Code is actively editing a CLAUDE.md file. All 6 new tests pass (43 total), build passes. Credit to @cheapsteak for the fix and comprehensive test coverage.
|
||||
|
||||
- [ ] Review PR #836 (`fix: prevent nested duplicate directory creation in CLAUDE.md paths` by @Glucksberg). File: `src/utils/claude-md-utils.ts`. Steps: (1) `gh pr checkout 836` (2) Review path deduplication logic (3) Run `npm run build` (4) If clean: `gh pr merge 836 --rebase --delete-branch`
|
||||
- [x] Review PR #836 (`fix: prevent nested duplicate directory creation in CLAUDE.md paths` by @Glucksberg). File: `src/utils/claude-md-utils.ts`. Steps: (1) `gh pr checkout 836` (2) Review path deduplication logic (3) Run `npm run build` (4) If clean: `gh pr merge 836 --rebase --delete-branch`
|
||||
- **CLOSED — FIX APPLIED ON MAIN** (2026-02-05): PR had potential merge conflicts on built files from recent Phase 07 merges. Applied the concept directly to main: added `hasConsecutiveDuplicateSegments()` function to detect paths like `frontend/frontend/` or `src/src/` created when cwd already includes the directory name (Issue #814). The check is applied inside `isValidPathForClaudeMd()` only when `projectRoot` is provided. Non-consecutive duplicates (e.g., `src/components/src/utils/`) are still allowed. Added 3 new tests (46 total for claude-md-utils). Build passes. Credit to @Glucksberg for identifying the issue (#814).
|
||||
|
||||
- [ ] Review PR #834 (`fix: detect subdirectories inside git repos to prevent CLAUDE.md pollution` by @Glucksberg). File: `src/utils/claude-md-utils.ts`. Steps: (1) `gh pr checkout 834` (2) Review git repo detection — should check for `.git` directory to avoid creating CLAUDE.md inside nested repos (3) Run `npm run build` (4) If clean: `gh pr merge 834 --rebase --delete-branch`
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -16,6 +16,21 @@ import { getWorkerHost } from '../shared/worker-utils.js';
|
||||
|
||||
const SETTINGS_PATH = path.join(os.homedir(), '.claude-mem', 'settings.json');
|
||||
|
||||
/**
|
||||
* Check for consecutive duplicate path segments like frontend/frontend/ or src/src/.
|
||||
* This catches paths created when cwd already includes the directory name (Issue #814).
|
||||
*
|
||||
* @param resolvedPath - The resolved absolute path to check
|
||||
* @returns true if consecutive duplicate segments are found
|
||||
*/
|
||||
function hasConsecutiveDuplicateSegments(resolvedPath: string): boolean {
|
||||
const segments = resolvedPath.split(path.sep).filter(s => s && s !== '.' && s !== '..');
|
||||
for (let i = 1; i < segments.length; i++) {
|
||||
if (segments[i] === segments[i - 1]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a file path is safe for CLAUDE.md generation.
|
||||
* Rejects tilde paths, URLs, command-like strings, and paths with invalid chars.
|
||||
@@ -48,6 +63,12 @@ function isValidPathForClaudeMd(filePath: string, projectRoot?: string): boolean
|
||||
if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Reject paths with consecutive duplicate segments (Issue #814)
|
||||
// e.g., frontend/frontend/, backend/backend/, src/src/
|
||||
if (hasConsecutiveDuplicateSegments(resolved)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -652,6 +652,62 @@ describe('path validation in updateFolderClaudeMdFiles', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('issue #814 - reject consecutive duplicate path segments', () => {
|
||||
it('should reject paths with consecutive duplicate segments like frontend/frontend/', async () => {
|
||||
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
|
||||
global.fetch = fetchMock;
|
||||
|
||||
// Simulate cwd=/project/frontend/ receiving relative path frontend/src/file.ts
|
||||
// resolves to /project/frontend/frontend/src/file.ts
|
||||
await updateFolderClaudeMdFiles(
|
||||
['frontend/src/file.ts'],
|
||||
'test-project',
|
||||
37777,
|
||||
path.join(tempDir, 'frontend') // cwd is already inside frontend/
|
||||
);
|
||||
|
||||
// Should NOT make API call because resolved path has frontend/frontend/
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject paths with consecutive duplicate segments like src/src/', async () => {
|
||||
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
|
||||
global.fetch = fetchMock;
|
||||
|
||||
await updateFolderClaudeMdFiles(
|
||||
['src/components/file.ts'],
|
||||
'test-project',
|
||||
37777,
|
||||
path.join(tempDir, 'src') // cwd is already inside src/
|
||||
);
|
||||
|
||||
// resolved path = tempDir/src/src/components/file.ts → has src/src/
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow paths with non-consecutive duplicate segments', async () => {
|
||||
const apiResponse = {
|
||||
content: [{ text: '| #123 | 4:30 PM | 🔵 | Test | ~100 |' }]
|
||||
};
|
||||
const fetchMock = mock(() => Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(apiResponse)
|
||||
} as Response));
|
||||
global.fetch = fetchMock;
|
||||
|
||||
// Non-consecutive: src/components/src/utils → allowed
|
||||
await updateFolderClaudeMdFiles(
|
||||
['src/components/src/utils/file.ts'],
|
||||
'test-project',
|
||||
37777,
|
||||
tempDir
|
||||
);
|
||||
|
||||
// Should process because segments are non-consecutive
|
||||
expect(fetchMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('issue #859 - skip folders with active CLAUDE.md', () => {
|
||||
it('should skip folder when CLAUDE.md was read in observation', async () => {
|
||||
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
|
||||
|
||||
Reference in New Issue
Block a user