fix: stop home installs copying .agents into ~/.claude and ~/.codex

This commit is contained in:
Affaan Mustafa
2026-09-18 18:37:05 -04:00
parent b15f7d8171
commit da214d73b7
4 changed files with 76 additions and 4 deletions
+3 -1
View File
@@ -1,6 +1,7 @@
const path = require('path');
const {
HOME_INSTALL_EXCLUDED_SOURCE_PATHS,
createInstallTargetAdapter,
createRemappedOperation,
isForeignPlatformPath,
@@ -52,6 +53,7 @@ module.exports = createInstallTargetAdapter({
kind: 'home',
rootSegments: ['.claude'],
installStatePathSegments: ['ecc', 'install-state.json'],
excludedSourcePaths: HOME_INSTALL_EXCLUDED_SOURCE_PATHS,
nativeRootRelativePath: '.claude-plugin',
planOperations(input, adapter) {
const modules = Array.isArray(input.modules)
@@ -66,7 +68,7 @@ module.exports = createInstallTargetAdapter({
return modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(p => !isForeignPlatformPath(p, adapter.target))
.filter(p => !isForeignPlatformPath(p, adapter.target) && !adapter.excludesSourcePath(p))
.flatMap(sourceRelativePath => {
if (
module.id === 'hooks-runtime'
+2 -1
View File
@@ -1,4 +1,4 @@
const { createInstallTargetAdapter } = require('./helpers');
const { HOME_INSTALL_EXCLUDED_SOURCE_PATHS, createInstallTargetAdapter } = require('./helpers');
module.exports = createInstallTargetAdapter({
id: 'codex-home',
@@ -7,4 +7,5 @@ module.exports = createInstallTargetAdapter({
rootSegments: ['.codex'],
installStatePathSegments: ['ecc-install-state.json'],
nativeRootRelativePath: '.codex',
excludedSourcePaths: HOME_INSTALL_EXCLUDED_SOURCE_PATHS,
});
+23 -2
View File
@@ -24,6 +24,14 @@ const PLATFORM_SOURCE_PATH_OWNERS = Object.freeze({
'.adal': 'adal',
});
// Source paths that home installs must never copy into a harness home
// directory. `.agents` is ECC's repo-local skills/plugins staging area:
// project targets such as kimi and antigravity consume it, but neither
// Claude Code nor Codex reads a `.agents` directory under ~/.claude or
// ~/.codex, so copying it there produces unread files that doctor flags as
// drift and repair keeps restoring.
const HOME_INSTALL_EXCLUDED_SOURCE_PATHS = Object.freeze(['.agents']);
function normalizeRelativePath(relativePath) {
return String(relativePath || '')
.replace(/\\/g, '/')
@@ -43,6 +51,14 @@ function isForeignPlatformPath(sourceRelativePath, adapterTarget) {
return false;
}
function isExcludedSourcePath(sourceRelativePath, excludedSourcePaths = []) {
const normalizedPath = normalizeRelativePath(sourceRelativePath);
return excludedSourcePaths.some(excluded => {
const prefix = normalizeRelativePath(excluded);
return prefix !== '' && (normalizedPath === prefix || normalizedPath.startsWith(`${prefix}/`));
});
}
function resolveBaseRoot(scope, input = {}) {
if (scope === 'home') {
return input.homeDir || os.homedir();
@@ -351,6 +367,9 @@ function createInstallTargetAdapter(config) {
strategy: adapter.determineStrategy(normalizedSourcePath),
});
},
excludesSourcePath(sourceRelativePath) {
return isExcludedSourcePath(sourceRelativePath, config.excludedSourcePaths);
},
planOperations(input = {}) {
if (typeof config.planOperations === 'function') {
return config.planOperations(input, adapter);
@@ -360,7 +379,7 @@ function createInstallTargetAdapter(config) {
return input.modules.flatMap(module => {
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(p => !isForeignPlatformPath(p, config.target))
.filter(p => !isForeignPlatformPath(p, config.target) && !adapter.excludesSourcePath(p))
.map(sourceRelativePath => adapter.createScaffoldOperation(
module.id,
sourceRelativePath,
@@ -372,7 +391,7 @@ function createInstallTargetAdapter(config) {
const module = input.module || {};
const paths = Array.isArray(module.paths) ? module.paths : [];
return paths
.filter(p => !isForeignPlatformPath(p, config.target))
.filter(p => !isForeignPlatformPath(p, config.target) && !adapter.excludesSourcePath(p))
.map(sourceRelativePath => adapter.createScaffoldOperation(
module.id,
sourceRelativePath,
@@ -399,6 +418,8 @@ function createInstallTargetAdapter(config) {
}
module.exports = {
HOME_INSTALL_EXCLUDED_SOURCE_PATHS,
isExcludedSourcePath,
buildValidationIssue,
createFlatFileOperations,
createFlatRuleOperations,
+48
View File
@@ -593,6 +593,54 @@ function runTests() {
}
})) passed++; else failed++;
if (test('home installs do not copy the repo .agents staging directory into Claude or Codex homes', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');
try {
const claudeResult = run(['--profile', 'core', '--enable-hooks'], { cwd: projectDir, homeDir });
assert.strictEqual(claudeResult.code, 0, claudeResult.stderr);
const claudeRoot = path.join(homeDir, '.claude');
assert.ok(fs.existsSync(path.join(claudeRoot, 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(claudeRoot, 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(
!fs.existsSync(path.join(claudeRoot, '.agents')),
'Claude home must not receive the repo .agents staging directory'
);
const claudeState = readJson(path.join(claudeRoot, 'ecc', 'install-state.json'));
assert.ok(
!claudeState.operations.some(operation => (
String(operation.sourceRelativePath || '').replace(/\\/g, '/').split('/')[0] === '.agents'
)),
'Claude install-state must not record .agents copy operations'
);
const codexResult = run(['--target', 'codex', '--profile', 'core'], { cwd: projectDir, homeDir });
assert.strictEqual(codexResult.code, 0, codexResult.stderr);
const codexRoot = path.join(homeDir, '.codex');
assert.ok(fs.existsSync(path.join(codexRoot, 'agents', 'architect.md')));
assert.ok(fs.existsSync(path.join(codexRoot, 'skills', 'tdd-workflow', 'SKILL.md')));
assert.ok(
!fs.existsSync(path.join(codexRoot, '.agents')),
'Codex home must not receive the repo .agents staging directory'
);
const codexState = readJson(path.join(codexRoot, 'ecc-install-state.json'));
assert.ok(
!codexState.operations.some(operation => (
String(operation.sourceRelativePath || '').replace(/\\/g, '/').split('/')[0] === '.agents'
)),
'Codex install-state must not record .agents copy operations'
);
} finally {
cleanup(homeDir);
cleanup(projectDir);
}
})) passed++; else failed++;
if (test('preserves existing top-level Claude rules and skills during managed install', () => {
const homeDir = createTempDir('install-apply-home-');
const projectDir = createTempDir('install-apply-project-');