claude-dev-env 8.88.3 → 8.88.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/install.mjs +58 -3
- package/bin/install.test.mjs +101 -0
- package/bin/profile-stop-gate.mjs +49 -0
- package/bin/profile-stop-gate.test.mjs +36 -0
- package/docs/agents-guidance/asks.md +1 -1
- package/package.json +1 -1
package/bin/install.mjs
CHANGED
|
@@ -133,6 +133,8 @@ import {
|
|
|
133
133
|
addOpenTasksGateToProfiles,
|
|
134
134
|
otherProfileSettingsPaths,
|
|
135
135
|
removeOpenTasksGateFromProfiles,
|
|
136
|
+
splitUnreadableProfileSettings,
|
|
137
|
+
unparsedSettingsMayHoldGate,
|
|
136
138
|
} from './profile-stop-gate.mjs';
|
|
137
139
|
import { pythonExecutableProbeCommand } from './python-probe.mjs';
|
|
138
140
|
|
|
@@ -2714,6 +2716,54 @@ function profileSettingsPathsForStopGate(mainSettingsPath) {
|
|
|
2714
2716
|
return otherProfileSettingsPaths({ homeDirectory: homedir(), environment: process.env, mainSettingsPath });
|
|
2715
2717
|
}
|
|
2716
2718
|
|
|
2719
|
+
/**
|
|
2720
|
+
* Return the profile settings the gate step can parse, and add an Action needed line for each one it cannot.
|
|
2721
|
+
*
|
|
2722
|
+
* @param {string} mainSettingsPath
|
|
2723
|
+
* @returns {string[]}
|
|
2724
|
+
*/
|
|
2725
|
+
function readableProfileSettingsPathsForStopGate(mainSettingsPath) {
|
|
2726
|
+
const { allReadablePaths, allUnreadable } = splitUnreadableProfileSettings(
|
|
2727
|
+
profileSettingsPathsForStopGate(mainSettingsPath),
|
|
2728
|
+
);
|
|
2729
|
+
for (const { settingsPath, reason } of allUnreadable) {
|
|
2730
|
+
const actionLine = `Profile settings: ${settingsPath} was left unchanged because it is not valid JSON (${reason}).`
|
|
2731
|
+
+ ' Fix the file and run the install again to add the open-tasks Stop gate there.';
|
|
2732
|
+
console.warn(` ${actionLine}`);
|
|
2733
|
+
allActionNeededLines.push(actionLine);
|
|
2734
|
+
}
|
|
2735
|
+
return allReadablePaths;
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* Return the profile settings the uninstall removes the gate from, or the reason it must stop first.
|
|
2740
|
+
*
|
|
2741
|
+
* A profile file that does not parse and may still hold the gate stops the
|
|
2742
|
+
* uninstall before it changes anything. The manifest then stays, so running
|
|
2743
|
+
* --uninstall again after fixing the file removes the gate. A file that does
|
|
2744
|
+
* not parse and does not name the gate has nothing to remove and is left as
|
|
2745
|
+
* it is.
|
|
2746
|
+
*
|
|
2747
|
+
* @param {string} mainSettingsPath
|
|
2748
|
+
* @returns {{ allReadablePaths: string[], blockingReason: string|null }}
|
|
2749
|
+
*/
|
|
2750
|
+
function profileSettingsPathsForGateRemoval(mainSettingsPath) {
|
|
2751
|
+
const { allReadablePaths, allUnreadable } = splitUnreadableProfileSettings(
|
|
2752
|
+
profileSettingsPathsForStopGate(mainSettingsPath),
|
|
2753
|
+
);
|
|
2754
|
+
const allHoldingGate = allUnreadable.filter(
|
|
2755
|
+
({ settingsPath }) => unparsedSettingsMayHoldGate(settingsPath, CLAUDE_HOME),
|
|
2756
|
+
);
|
|
2757
|
+
if (allHoldingGate.length === 0) return { allReadablePaths, blockingReason: null };
|
|
2758
|
+
const fileLines = allHoldingGate.map(({ settingsPath, reason }) => ` ${settingsPath}: not valid JSON (${reason})`);
|
|
2759
|
+
const blockingReason = [
|
|
2760
|
+
'ERROR: uninstall stopped before changing anything. These profile settings files still name the open-tasks Stop gate, and they are not valid JSON, so the gate cannot be removed from them:',
|
|
2761
|
+
...fileLines,
|
|
2762
|
+
'Fix each file and run --uninstall again.',
|
|
2763
|
+
].join('\n');
|
|
2764
|
+
return { allReadablePaths, blockingReason };
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2717
2767
|
function executeInstallPlan(plan) {
|
|
2718
2768
|
const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
|
|
2719
2769
|
const snapshot = capturePriorInstallSnapshot({
|
|
@@ -3208,7 +3258,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
3208
3258
|
for (const eachOutcome of addOpenTasksGateToProfiles({
|
|
3209
3259
|
mainSettingsPath: join(CLAUDE_HOME, SETTINGS_FILE_NAME),
|
|
3210
3260
|
claudeHome: CLAUDE_HOME,
|
|
3211
|
-
allProfileSettingsPaths:
|
|
3261
|
+
allProfileSettingsPaths: readableProfileSettingsPathsForStopGate(join(CLAUDE_HOME, SETTINGS_FILE_NAME)),
|
|
3212
3262
|
homeDirectory: homedir(),
|
|
3213
3263
|
})) {
|
|
3214
3264
|
if (eachOutcome.additions.length > 0) {
|
|
@@ -3498,7 +3548,7 @@ function executeUninstallPlan(plan, helpers = {}) {
|
|
|
3498
3548
|
}
|
|
3499
3549
|
for (const eachChangedPath of removeOpenTasksGateFromProfiles({
|
|
3500
3550
|
claudeHome: CLAUDE_HOME,
|
|
3501
|
-
allProfileSettingsPaths:
|
|
3551
|
+
allProfileSettingsPaths: helpers.allProfileSettingsPaths,
|
|
3502
3552
|
})) {
|
|
3503
3553
|
console.log(` Profile settings: open-tasks Stop gate removed from ${eachChangedPath}`);
|
|
3504
3554
|
}
|
|
@@ -3554,6 +3604,11 @@ function uninstall() {
|
|
|
3554
3604
|
if (plan.isNoOp) {
|
|
3555
3605
|
return;
|
|
3556
3606
|
}
|
|
3607
|
+
const gateRemoval = profileSettingsPathsForGateRemoval(plan.settingsPath);
|
|
3608
|
+
if (gateRemoval.blockingReason) {
|
|
3609
|
+
console.error(gateRemoval.blockingReason);
|
|
3610
|
+
process.exit(1);
|
|
3611
|
+
}
|
|
3557
3612
|
|
|
3558
3613
|
const faultPhase = resolveFaultPhaseFromEnvironment(process.env);
|
|
3559
3614
|
const snapshot = capturePriorInstallSnapshot({
|
|
@@ -3576,7 +3631,7 @@ function uninstall() {
|
|
|
3576
3631
|
snapshot,
|
|
3577
3632
|
faultPhase,
|
|
3578
3633
|
runMutations: ({ throwIfFault }) => {
|
|
3579
|
-
executeUninstallPlan(plan, { throwIfFault });
|
|
3634
|
+
executeUninstallPlan(plan, { throwIfFault, allProfileSettingsPaths: gateRemoval.allReadablePaths });
|
|
3580
3635
|
},
|
|
3581
3636
|
});
|
|
3582
3637
|
} catch (mutationError) {
|
package/bin/install.test.mjs
CHANGED
|
@@ -3726,3 +3726,104 @@ test('install adds the open-tasks Stop gate to every profile with its own settin
|
|
|
3726
3726
|
assert.equal(uninstallation.status, 0, uninstallation.stderr);
|
|
3727
3727
|
assert.deepEqual(JSON.parse(readFileSync(profileSettingsPath, 'utf8')), { outputStyle: 'plain' });
|
|
3728
3728
|
});
|
|
3729
|
+
|
|
3730
|
+
test('a profile settings.json that is not valid JSON is left alone and named under Action needed, and the rest of the install and uninstall go through', (context) => {
|
|
3731
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cde-profile-gate-malformed-'));
|
|
3732
|
+
context.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3733
|
+
const brokenSettingsPath = join(homeDirectory, '.claude-profiles', 'broken', 'settings.json');
|
|
3734
|
+
const goodSettingsPath = join(homeDirectory, '.claude-profiles', 'good', 'settings.json');
|
|
3735
|
+
const brokenSettingsText = '{"hooks":{},}\n';
|
|
3736
|
+
mkdirSync(dirname(brokenSettingsPath), { recursive: true });
|
|
3737
|
+
mkdirSync(dirname(goodSettingsPath), { recursive: true });
|
|
3738
|
+
writeFileSync(brokenSettingsPath, brokenSettingsText);
|
|
3739
|
+
writeFileSync(goodSettingsPath, JSON.stringify({ outputStyle: 'plain' }, null, 4));
|
|
3740
|
+
const runInstaller = (argumentsList) => spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'install.mjs'), ...argumentsList], {
|
|
3741
|
+
cwd: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
3742
|
+
encoding: 'utf8',
|
|
3743
|
+
env: {
|
|
3744
|
+
...process.env,
|
|
3745
|
+
CLAUDE_CONFIG_DIR: undefined,
|
|
3746
|
+
LLM_SETTINGS_PROFILES_ROOT: undefined,
|
|
3747
|
+
CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
|
|
3748
|
+
CDE_INSTALL_PSTACK: '0',
|
|
3749
|
+
CDE_INSTALL_DEEPSTACK: '0',
|
|
3750
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3751
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3752
|
+
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
3753
|
+
HOME: homeDirectory,
|
|
3754
|
+
USERPROFILE: homeDirectory,
|
|
3755
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3756
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3757
|
+
TEMP: homeDirectory,
|
|
3758
|
+
TMP: homeDirectory,
|
|
3759
|
+
},
|
|
3760
|
+
});
|
|
3761
|
+
const goodGateCount = () => (JSON.parse(readFileSync(goodSettingsPath, 'utf8')).hooks?.Stop ?? [])
|
|
3762
|
+
.flatMap((eachGroup) => eachGroup.hooks.map((eachHook) => eachHook.command))
|
|
3763
|
+
.filter((eachCommand) => eachCommand.includes('open_tasks_stop_gate.py')).length;
|
|
3764
|
+
const actionNeededSection = (stderrText) => stderrText.slice(stderrText.indexOf('Action needed'));
|
|
3765
|
+
|
|
3766
|
+
const installation = runInstaller([]);
|
|
3767
|
+
assert.equal(installation.status, 0, installation.stderr);
|
|
3768
|
+
assert.equal(readFileSync(brokenSettingsPath, 'utf8'), brokenSettingsText);
|
|
3769
|
+
assert.equal(goodGateCount(), 1);
|
|
3770
|
+
assert.match(actionNeededSection(installation.stderr), /Action needed \(1\):/);
|
|
3771
|
+
assert.ok(actionNeededSection(installation.stderr).includes(brokenSettingsPath));
|
|
3772
|
+
assert.match(actionNeededSection(installation.stderr), /not valid JSON \(.+\)/);
|
|
3773
|
+
|
|
3774
|
+
const uninstallation = runInstaller(['--uninstall']);
|
|
3775
|
+
assert.equal(uninstallation.status, 0, uninstallation.stderr);
|
|
3776
|
+
assert.equal(readFileSync(brokenSettingsPath, 'utf8'), brokenSettingsText);
|
|
3777
|
+
assert.equal(goodGateCount(), 0);
|
|
3778
|
+
});
|
|
3779
|
+
|
|
3780
|
+
test('uninstall stops before changing anything while a profile settings.json that is not valid JSON still names the gate, and a rerun after the fix removes it', (context) => {
|
|
3781
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cde-profile-gate-uninstall-malformed-'));
|
|
3782
|
+
context.after(() => rmSync(homeDirectory, { recursive: true, force: true }));
|
|
3783
|
+
const profileSettingsPath = join(homeDirectory, '.claude-profiles', 'prefs-only', 'settings.json');
|
|
3784
|
+
mkdirSync(dirname(profileSettingsPath), { recursive: true });
|
|
3785
|
+
writeFileSync(profileSettingsPath, JSON.stringify({ outputStyle: 'plain' }, null, 4));
|
|
3786
|
+
const runInstaller = (argumentsList) => spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), 'install.mjs'), ...argumentsList], {
|
|
3787
|
+
cwd: dirname(dirname(fileURLToPath(import.meta.url))),
|
|
3788
|
+
encoding: 'utf8',
|
|
3789
|
+
env: {
|
|
3790
|
+
...process.env,
|
|
3791
|
+
CLAUDE_CONFIG_DIR: undefined,
|
|
3792
|
+
LLM_SETTINGS_PROFILES_ROOT: undefined,
|
|
3793
|
+
CLAUDE_DEV_ENV_INSTALL_FAULT: undefined,
|
|
3794
|
+
CDE_INSTALL_PSTACK: '0',
|
|
3795
|
+
CDE_INSTALL_DEEPSTACK: '0',
|
|
3796
|
+
CDE_INSTALL_USAGE_WRAPUP: '0',
|
|
3797
|
+
CDE_INSTALL_SUBAGENT_MODELS: '0',
|
|
3798
|
+
CDE_INSTALL_DEEPEN_FIRST_PROMPT: '0',
|
|
3799
|
+
HOME: homeDirectory,
|
|
3800
|
+
USERPROFILE: homeDirectory,
|
|
3801
|
+
CODEX_HOME: join(homeDirectory, '.codex'),
|
|
3802
|
+
GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
|
|
3803
|
+
TEMP: homeDirectory,
|
|
3804
|
+
TMP: homeDirectory,
|
|
3805
|
+
},
|
|
3806
|
+
});
|
|
3807
|
+
const manifestPath = join(homeDirectory, '.claude', '.claude-dev-env-manifest.json');
|
|
3808
|
+
|
|
3809
|
+
const installation = runInstaller([]);
|
|
3810
|
+
assert.equal(installation.status, 0, installation.stderr);
|
|
3811
|
+
const validText = readFileSync(profileSettingsPath, 'utf8');
|
|
3812
|
+
assert.ok(validText.includes('open_tasks_stop_gate.py'));
|
|
3813
|
+
const brokenText = validText.trimEnd().replace(/\}$/, ',}') + '\n';
|
|
3814
|
+
writeFileSync(profileSettingsPath, brokenText);
|
|
3815
|
+
|
|
3816
|
+
const stoppedUninstall = runInstaller(['--uninstall']);
|
|
3817
|
+
assert.equal(stoppedUninstall.status, 1, stoppedUninstall.stdout);
|
|
3818
|
+
assert.ok(stoppedUninstall.stderr.includes(profileSettingsPath), stoppedUninstall.stderr);
|
|
3819
|
+
assert.match(stoppedUninstall.stderr, /not valid JSON \(.+\)/);
|
|
3820
|
+
assert.match(stoppedUninstall.stderr, /run --uninstall again/);
|
|
3821
|
+
assert.equal(readFileSync(profileSettingsPath, 'utf8'), brokenText);
|
|
3822
|
+
assert.ok(existsSync(manifestPath));
|
|
3823
|
+
|
|
3824
|
+
writeFileSync(profileSettingsPath, validText);
|
|
3825
|
+
const rerunUninstall = runInstaller(['--uninstall']);
|
|
3826
|
+
assert.equal(rerunUninstall.status, 0, rerunUninstall.stderr);
|
|
3827
|
+
assert.deepEqual(JSON.parse(readFileSync(profileSettingsPath, 'utf8')), { outputStyle: 'plain' });
|
|
3828
|
+
assert.equal(existsSync(manifestPath), false);
|
|
3829
|
+
});
|
|
@@ -82,6 +82,55 @@ export function otherProfileSettingsPaths(options) {
|
|
|
82
82
|
return allOtherPaths;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Split profile settings paths into the ones whose settings.json parses and the ones that do not.
|
|
87
|
+
*
|
|
88
|
+
* A profile file that is not valid JSON is left as it is. The caller names
|
|
89
|
+
* each one, with its parse error, so the user can fix it and run the install
|
|
90
|
+
* again. An empty file counts as readable.
|
|
91
|
+
*
|
|
92
|
+
* @param {string[]} allProfileSettingsPaths
|
|
93
|
+
* @returns {{ allReadablePaths: string[], allUnreadable: { settingsPath: string, reason: string }[] }}
|
|
94
|
+
*/
|
|
95
|
+
export function splitUnreadableProfileSettings(allProfileSettingsPaths) {
|
|
96
|
+
const allReadablePaths = [];
|
|
97
|
+
const allUnreadable = [];
|
|
98
|
+
for (const eachPath of allProfileSettingsPaths) {
|
|
99
|
+
try {
|
|
100
|
+
const settingsText = readFileSync(eachPath, 'utf8');
|
|
101
|
+
if (settingsText.trim()) JSON.parse(settingsText);
|
|
102
|
+
allReadablePaths.push(eachPath);
|
|
103
|
+
} catch (readError) {
|
|
104
|
+
allUnreadable.push({
|
|
105
|
+
settingsPath: eachPath,
|
|
106
|
+
reason: readError instanceof Error ? readError.message : String(readError),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { allReadablePaths, allUnreadable };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* True when a settings.json that does not parse may still hold this Claude home's open-tasks gate.
|
|
115
|
+
*
|
|
116
|
+
* The raw text is searched for the gate script path, with JSON-escaped
|
|
117
|
+
* backslashes read as path separators. A file that cannot be read counts as
|
|
118
|
+
* one that may hold the gate.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} settingsPath
|
|
121
|
+
* @param {string} claudeHome
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
export function unparsedSettingsMayHoldGate(settingsPath, claudeHome) {
|
|
125
|
+
let settingsText;
|
|
126
|
+
try {
|
|
127
|
+
settingsText = readFileSync(settingsPath, 'utf8');
|
|
128
|
+
} catch {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
return comparableCommand(settingsText.replace(/\\\\/g, '/')).includes(gateScriptCommandFragment(claudeHome));
|
|
132
|
+
}
|
|
133
|
+
|
|
85
134
|
function stopHooks(settingsPath) {
|
|
86
135
|
const settingsText = readFileSync(settingsPath, 'utf8');
|
|
87
136
|
if (!settingsText.trim()) return [];
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
addOpenTasksGateToProfiles,
|
|
9
9
|
otherProfileSettingsPaths,
|
|
10
10
|
removeOpenTasksGateFromProfiles,
|
|
11
|
+
splitUnreadableProfileSettings,
|
|
12
|
+
unparsedSettingsMayHoldGate,
|
|
11
13
|
} from './profile-stop-gate.mjs';
|
|
12
14
|
|
|
13
15
|
function makeHome(context) {
|
|
@@ -122,3 +124,37 @@ test('a profile that already runs its own copy of the gate script gets no second
|
|
|
122
124
|
assert.deepEqual(allOutcomes, []);
|
|
123
125
|
assert.deepEqual(stopCommands(profilePath), [ownGateCommand]);
|
|
124
126
|
});
|
|
127
|
+
|
|
128
|
+
test('a settings.json that is not valid JSON is reported with its parse error, and an empty one counts as readable', (context) => {
|
|
129
|
+
const { homeDirectory } = makeHome(context);
|
|
130
|
+
const goodPath = writeProfile(homeDirectory, 'good', { outputStyle: 'plain' });
|
|
131
|
+
const emptyPath = writeProfile(homeDirectory, 'empty');
|
|
132
|
+
writeFileSync(emptyPath, '');
|
|
133
|
+
const brokenPath = writeProfile(homeDirectory, 'broken');
|
|
134
|
+
writeFileSync(brokenPath, '{"hooks":{},}');
|
|
135
|
+
|
|
136
|
+
const { allReadablePaths, allUnreadable } = splitUnreadableProfileSettings([goodPath, emptyPath, brokenPath]);
|
|
137
|
+
|
|
138
|
+
assert.deepEqual(allReadablePaths, [goodPath, emptyPath]);
|
|
139
|
+
assert.equal(allUnreadable.length, 1);
|
|
140
|
+
assert.equal(allUnreadable[0].settingsPath, brokenPath);
|
|
141
|
+
assert.match(allUnreadable[0].reason, /JSON/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a settings.json that does not parse may hold the gate when its text names this home\'s gate script, in either slash spelling', (context) => {
|
|
145
|
+
const { homeDirectory, claudeHome, gateCommand } = makeHome(context);
|
|
146
|
+
const forwardPath = writeProfile(homeDirectory, 'forward');
|
|
147
|
+
writeFileSync(forwardPath, `{"hooks":{"Stop":[{"hooks":[{"command":"${gateCommand}"}]}]},}`);
|
|
148
|
+
const escapedPath = writeProfile(homeDirectory, 'escaped');
|
|
149
|
+
writeFileSync(escapedPath, `{"hooks":{"Stop":[{"hooks":[{"command":"${gateCommand.replace(/\//g, '\\\\')}"}]}]},}`);
|
|
150
|
+
const otherHomePath = writeProfile(homeDirectory, 'other-home');
|
|
151
|
+
writeFileSync(otherHomePath, '{"hooks":{"Stop":[{"hooks":[{"command":"python3 /opt/other/.claude/hooks/blocking/open_tasks_stop_gate.py"}]}]},}');
|
|
152
|
+
const noGatePath = writeProfile(homeDirectory, 'no-gate');
|
|
153
|
+
writeFileSync(noGatePath, '{"hooks":{},}');
|
|
154
|
+
|
|
155
|
+
assert.equal(unparsedSettingsMayHoldGate(forwardPath, claudeHome), true);
|
|
156
|
+
assert.equal(unparsedSettingsMayHoldGate(escapedPath, claudeHome), true);
|
|
157
|
+
assert.equal(unparsedSettingsMayHoldGate(otherHomePath, claudeHome), false);
|
|
158
|
+
assert.equal(unparsedSettingsMayHoldGate(noGatePath, claudeHome), false);
|
|
159
|
+
assert.equal(unparsedSettingsMayHoldGate(join(homeDirectory, 'missing', 'settings.json'), claudeHome), true);
|
|
160
|
+
});
|
|
@@ -25,7 +25,7 @@ When the host keeps an asks ledger with `ledger.py` and `asks_check.py`, record
|
|
|
25
25
|
|
|
26
26
|
The installer registers `open_tasks_stop_gate.py` as a Stop hook for each host:
|
|
27
27
|
|
|
28
|
-
- Claude Code: `hooks.json`. A block returns the open items as the reason, and the session keeps working. An install into `~/.claude` also adds the same Stop entry to every profile under `~/.claude-profiles` that has its own `settings.json` and does not already run its own copy of the gate. A profile whose `settings.json` links to `~/.claude/settings.json` already has it. Uninstall removes only that entry.
|
|
28
|
+
- Claude Code: `hooks.json`. A block returns the open items as the reason, and the session keeps working. An install into `~/.claude` also adds the same Stop entry to every profile under `~/.claude-profiles` that has its own `settings.json` and does not already run its own copy of the gate. A profile whose `settings.json` links to `~/.claude/settings.json` already has it. Uninstall removes only that entry. On install, a profile `settings.json` that is not valid JSON is left unchanged and listed under Action needed with its path and parse error, and the rest of the install goes on. On uninstall, such a file that still names the gate stops the uninstall before anything changes, so the manifest stays and a rerun after the fix removes the gate; one that does not name the gate is left as it is.
|
|
29
29
|
- Codex: `$CODEX_HOME/hooks.json`. Codex turns the reason into a continuation prompt.
|
|
30
30
|
- Cursor: a native `stop` entry in `~/.cursor/hooks.json`. The reason comes back as `followup_message`, the next user message. A stop with status `aborted` or `error` passes, so a turn the user cancels stays silent.
|
|
31
31
|
|