MAESTRO: Prevent CLAUDE.md generation in unsafe directories (PR #929 concept)

Add exclusion list for directories where CLAUDE.md generation breaks
toolchains: res/ (Android aapt2), .git/, build/, node_modules/,
__pycache__/. Closes issue #912. Credit to @jayvenn21.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Newman
2026-02-06 01:35:31 -05:00
parent e424d85a93
commit ab3d4ca865
4 changed files with 181 additions and 47 deletions
+2 -1
View File
@@ -31,7 +31,8 @@ These all modify `src/utils/claude-md-utils.ts` — review together.
- [x] 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`
- **CLOSED — WOULD DISABLE FOLDER CLAUDE.MD FEATURE** (2026-02-06): PR replaces `isProjectRoot()` (checks if folder directly contains `.git`) with `isInsideGitRepo()` (walks up parent directories). Since `isInsideGitRepo()` returns `true` for ALL subdirectories inside any git repo, this would skip CLAUDE.md generation for every subfolder in every git project — effectively disabling the core folder CLAUDE.md feature. The underlying pollution issues (#793, #778) are real but have been addressed through targeted fixes: PR #836 (duplicate path segments), PR #974 (race conditions), and path validation improvements. Remaining unsafe-directory issues (`.git/refs/`, `node_modules/`) are addressed by PR #929's exclusion list approach. Credit to @Glucksberg for flagging the issue.
- [ ] Review PR #929 (`Prevent CLAUDE.md generation in Android res/ and other unsafe directories` by @jayvenn21). File: `src/utils/claude-md-utils.ts`. Steps: (1) `gh pr checkout 929` (2) Review exclusion list — should include `res/`, `node_modules/`, `.git/`, etc. (3) Run `npm run build` (4) If clean: `gh pr merge 929 --rebase --delete-branch`
- [x] Review PR #929 (`Prevent CLAUDE.md generation in Android res/ and other unsafe directories` by @jayvenn21). File: `src/utils/claude-md-utils.ts`. Steps: (1) `gh pr checkout 929` (2) Review exclusion list — should include `res/`, `node_modules/`, `.git/`, etc. (3) Run `npm run build` (4) If clean: `gh pr merge 929 --rebase --delete-branch`
- **CLOSED — FIX APPLIED ON MAIN** (2026-02-06): PR was based on an older version of the file and would have merge conflicts on the insertion point. Applied the exact approach to main: added `EXCLUDED_UNSAFE_DIRECTORIES` Set containing `res`, `.git`, `build`, `node_modules`, `__pycache__` and `isExcludedUnsafeDirectory()` function that checks if any path segment matches the exclusion list. The check is placed in `updateFolderClaudeMdFiles` after the project root check but before the active CLAUDE.md race condition check. Added 7 new tests (53 total for claude-md-utils) covering all excluded directories, deeply nested cases, and safe directory pass-through. Build passes. Credit to @jayvenn21 for identifying issue #912 and proposing a clean solution.
## Folder CLAUDE.md Setting (winner from Phase 02 dedup)
File diff suppressed because one or more lines are too long
+26
View File
@@ -257,6 +257,27 @@ export function formatTimelineForClaudeMd(timelineText: string): string {
return lines.join('\n').trim();
}
/**
* Built-in directory names where CLAUDE.md generation is unsafe or undesirable.
* e.g. Android res/ is compiler-strict (non-XML breaks build); .git, build, node_modules are tooling-owned.
*/
const EXCLUDED_UNSAFE_DIRECTORIES = new Set([
'res',
'.git',
'build',
'node_modules',
'__pycache__'
]);
/**
* Returns true if folder path contains any excluded segment (e.g. .../res/..., .../node_modules/...).
*/
function isExcludedUnsafeDirectory(folderPath: string): boolean {
const normalized = path.normalize(folderPath);
const segments = normalized.split(path.sep);
return segments.some(segment => EXCLUDED_UNSAFE_DIRECTORIES.has(segment));
}
/**
* Check if a folder is a project root (contains .git directory).
* Project root CLAUDE.md files should remain user-managed, not auto-updated.
@@ -331,6 +352,11 @@ export async function updateFolderClaudeMdFiles(
logger.debug('FOLDER_INDEX', 'Skipping project root CLAUDE.md', { folderPath });
continue;
}
// Skip known-unsafe directories (e.g. Android res/, .git, build, node_modules)
if (isExcludedUnsafeDirectory(folderPath)) {
logger.debug('FOLDER_INDEX', 'Skipping unsafe directory for CLAUDE.md', { folderPath });
continue;
}
// Skip folders where CLAUDE.md was read/modified in this observation (issue #859)
if (foldersWithActiveClaudeMd.has(folderPath)) {
logger.debug('FOLDER_INDEX', 'Skipping folder with active CLAUDE.md to avoid race condition', { folderPath });
+107
View File
@@ -834,3 +834,110 @@ describe('issue #859 - skip folders with active CLAUDE.md', () => {
expect(fetchMock).not.toHaveBeenCalled();
});
});
describe('issue #912 - skip unsafe directories for CLAUDE.md generation', () => {
it('should skip node_modules directories', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['node_modules/lodash/index.js'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should skip .git directories', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['.git/refs/heads/main'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should skip Android res/ directories', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['app/src/main/res/layout/activity_main.xml'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should skip build/ directories', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['build/outputs/apk/debug/app-debug.apk'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should skip __pycache__/ directories', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
await updateFolderClaudeMdFiles(
['src/__pycache__/module.cpython-311.pyc'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
it('should allow safe directories like src/', 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;
await updateFolderClaudeMdFiles(
['src/utils/file.ts'],
'test-project',
37777,
tempDir
);
expect(fetchMock).toHaveBeenCalledTimes(1);
});
it('should skip deeply nested unsafe directories', async () => {
const fetchMock = mock(() => Promise.resolve({ ok: true } as Response));
global.fetch = fetchMock;
// node_modules nested deep inside project
await updateFolderClaudeMdFiles(
['packages/frontend/node_modules/react/index.js'],
'test-project',
37777,
tempDir
);
expect(fetchMock).not.toHaveBeenCalled();
});
});