From f662654afb8e7a230448655265d6f41adf6df62c Mon Sep 17 00:00:00 2001 From: Ben Vargas Date: Mon, 14 Jul 2025 02:29:36 -0600 Subject: [PATCH] fix: prevent CLAUDE.md overwrite by using imports (#949) * fix: prevent CLAUDE.md overwrite by using imports - Copy Task Master instructions to .taskmaster/CLAUDE.md - Add import section to user's CLAUDE.md instead of overwriting - Preserve existing user content - Clean removal of Task Master content on uninstall Closes #929 * chore: add changeset for Claude import fix --- .changeset/claude-import-fix-new.md | 12 ++ src/profiles/claude.js | 130 +++++++++++++++++- .../claude-init-functionality.test.js | 10 +- 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 .changeset/claude-import-fix-new.md diff --git a/.changeset/claude-import-fix-new.md b/.changeset/claude-import-fix-new.md new file mode 100644 index 00000000..acaf95d3 --- /dev/null +++ b/.changeset/claude-import-fix-new.md @@ -0,0 +1,12 @@ +--- +"task-master-ai": patch +--- + +Prevent CLAUDE.md overwrite by using Claude Code's import feature + +- Task Master now creates its instructions in `.taskmaster/CLAUDE.md` instead of overwriting the user's `CLAUDE.md` +- Adds an import section to the user's CLAUDE.md that references the Task Master instructions +- Preserves existing user content in CLAUDE.md files +- Provides clean uninstall that only removes Task Master's additions + +**Breaking Change**: Task Master instructions for Claude Code are now stored in `.taskmaster/CLAUDE.md` and imported into the main CLAUDE.md file. Users who previously had Task Master content directly in their CLAUDE.md will need to run `task-master rules remove claude` followed by `task-master rules add claude` to migrate to the new structure. \ No newline at end of file diff --git a/src/profiles/claude.js b/src/profiles/claude.js index 4ce7e557..2fc347f0 100644 --- a/src/profiles/claude.js +++ b/src/profiles/claude.js @@ -59,6 +59,63 @@ function onAddRulesProfile(targetDir, assetsDir) { `[Claude] An error occurred during directory copy: ${err.message}` ); } + + // Handle CLAUDE.md import for non-destructive integration + const sourceFile = path.join(assetsDir, 'AGENTS.md'); + const userClaudeFile = path.join(targetDir, 'CLAUDE.md'); + const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md'); + const importLine = '@./.taskmaster/CLAUDE.md'; + const importSection = `\n## Task Master AI Instructions\n**Import Task Master's development workflow commands and guidelines, treat as if import is in the main CLAUDE.md file.**\n${importLine}`; + + if (fs.existsSync(sourceFile)) { + try { + // Ensure .taskmaster directory exists + const taskMasterDir = path.join(targetDir, '.taskmaster'); + if (!fs.existsSync(taskMasterDir)) { + fs.mkdirSync(taskMasterDir, { recursive: true }); + } + + // Copy Task Master instructions to .taskmaster/CLAUDE.md + fs.copyFileSync(sourceFile, taskMasterClaudeFile); + log( + 'debug', + `[Claude] Created Task Master instructions at ${taskMasterClaudeFile}` + ); + + // Handle user's CLAUDE.md + if (fs.existsSync(userClaudeFile)) { + // Check if import already exists + const content = fs.readFileSync(userClaudeFile, 'utf8'); + if (!content.includes(importLine)) { + // Append import section at the end + const updatedContent = content.trim() + '\n' + importSection + '\n'; + fs.writeFileSync(userClaudeFile, updatedContent); + log( + 'info', + `[Claude] Added Task Master import to existing ${userClaudeFile}` + ); + } else { + log( + 'info', + `[Claude] Task Master import already present in ${userClaudeFile}` + ); + } + } else { + // Create minimal CLAUDE.md with the import section + const minimalContent = `# Claude Code Instructions\n${importSection}\n`; + fs.writeFileSync(userClaudeFile, minimalContent); + log( + 'info', + `[Claude] Created ${userClaudeFile} with Task Master import` + ); + } + } catch (err) { + log( + 'error', + `[Claude] Failed to set up Claude instructions: ${err.message}` + ); + } + } } function onRemoveRulesProfile(targetDir) { @@ -67,6 +124,77 @@ function onRemoveRulesProfile(targetDir) { if (removeDirectoryRecursive(claudeDir)) { log('debug', `[Claude] Removed .claude directory from ${claudeDir}`); } + + // Clean up CLAUDE.md import + const userClaudeFile = path.join(targetDir, 'CLAUDE.md'); + const taskMasterClaudeFile = path.join(targetDir, '.taskmaster', 'CLAUDE.md'); + const importLine = '@./.taskmaster/CLAUDE.md'; + + try { + // Remove Task Master CLAUDE.md from .taskmaster + if (fs.existsSync(taskMasterClaudeFile)) { + fs.rmSync(taskMasterClaudeFile, { force: true }); + log('debug', `[Claude] Removed ${taskMasterClaudeFile}`); + } + + // Clean up import from user's CLAUDE.md + if (fs.existsSync(userClaudeFile)) { + const content = fs.readFileSync(userClaudeFile, 'utf8'); + const lines = content.split('\n'); + const filteredLines = []; + let skipNextLines = 0; + + // Remove the Task Master section + for (let i = 0; i < lines.length; i++) { + if (skipNextLines > 0) { + skipNextLines--; + continue; + } + + // Check if this is the start of our Task Master section + if (lines[i].includes('## Task Master AI Instructions')) { + // Skip this line and the next two lines (bold text and import) + skipNextLines = 2; + continue; + } + + // Also remove standalone import lines (for backward compatibility) + if (lines[i].trim() === importLine) { + continue; + } + + filteredLines.push(lines[i]); + } + + // Join back and clean up excessive newlines + let updatedContent = filteredLines + .join('\n') + .replace(/\n{3,}/g, '\n\n') + .trim(); + + // Check if file only contained our minimal template + if ( + updatedContent === '# Claude Code Instructions' || + updatedContent === '' + ) { + // File only contained our import, remove it + fs.rmSync(userClaudeFile, { force: true }); + log('debug', `[Claude] Removed empty ${userClaudeFile}`); + } else { + // Write back without the import + fs.writeFileSync(userClaudeFile, updatedContent + '\n'); + log( + 'debug', + `[Claude] Removed Task Master import from ${userClaudeFile}` + ); + } + } + } catch (err) { + log( + 'error', + `[Claude] Failed to remove Claude instructions: ${err.message}` + ); + } } function onPostConvertRulesProfile(targetDir, assetsDir) { @@ -86,7 +214,7 @@ export const claudeProfile = createProfile({ mcpConfigName: null, includeDefaultRules: false, fileMap: { - 'AGENTS.md': 'CLAUDE.md' + 'AGENTS.md': '.taskmaster/CLAUDE.md' }, onAdd: onAddRulesProfile, onRemove: onRemoveRulesProfile, diff --git a/tests/integration/profiles/claude-init-functionality.test.js b/tests/integration/profiles/claude-init-functionality.test.js index b597da8c..ed623630 100644 --- a/tests/integration/profiles/claude-init-functionality.test.js +++ b/tests/integration/profiles/claude-init-functionality.test.js @@ -23,7 +23,9 @@ describe('Claude Profile Initialization Functionality', () => { expect(claudeProfileContent).toContain("rulesDir: '.'"); // non-default expect(claudeProfileContent).toContain('mcpConfig: false'); // non-default expect(claudeProfileContent).toContain('includeDefaultRules: false'); // non-default - expect(claudeProfileContent).toContain("'AGENTS.md': 'CLAUDE.md'"); + expect(claudeProfileContent).toContain( + "'AGENTS.md': '.taskmaster/CLAUDE.md'" + ); // Check the final computed properties on the profile object expect(claudeProfile.profileName).toBe('claude'); @@ -33,7 +35,7 @@ describe('Claude Profile Initialization Functionality', () => { expect(claudeProfile.mcpConfig).toBe(false); expect(claudeProfile.mcpConfigName).toBe(null); // computed expect(claudeProfile.includeDefaultRules).toBe(false); - expect(claudeProfile.fileMap['AGENTS.md']).toBe('CLAUDE.md'); + expect(claudeProfile.fileMap['AGENTS.md']).toBe('.taskmaster/CLAUDE.md'); }); test('claude.js has lifecycle functions for file management', () => { @@ -44,9 +46,11 @@ describe('Claude Profile Initialization Functionality', () => { ); }); - test('claude.js handles .claude directory in lifecycle functions', () => { + test('claude.js handles .claude directory and .taskmaster/CLAUDE.md import in lifecycle functions', () => { expect(claudeProfileContent).toContain('.claude'); expect(claudeProfileContent).toContain('copyRecursiveSync'); + expect(claudeProfileContent).toContain('.taskmaster/CLAUDE.md'); + expect(claudeProfileContent).toContain('@./.taskmaster/CLAUDE.md'); }); test('claude.js has proper error handling in lifecycle functions', () => {