fix: validate hook field requirements to prevent silent settings.json rejection
Add validateHookFields() that strips invalid hook entries before they cause Claude Code's Zod schema to silently discard the entire settings.json file. Agent hooks require "prompt", command hooks require "command", and entries without a valid hooks sub-array are removed. Uses a clean two-pass approach: first validate and build new arrays (no mutation inside filter predicates), then collect-and-delete empty event keys (no delete during Object.keys iteration). Result entries are shallow copies so the original input objects are never mutated. Includes 24 tests covering passthrough, removal, structural invalidity, empty cleanup, mutation safety, unknown types, and iteration safety. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2910,6 +2910,85 @@ function cleanupOrphanedHooks(settings) {
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate hook field requirements to prevent silent settings.json rejection.
|
||||
*
|
||||
* Claude Code validates the entire settings file with a strict Zod schema.
|
||||
* If ANY hook has an invalid schema (e.g., type: "agent" missing "prompt"),
|
||||
* the ENTIRE settings.json is silently discarded — disabling all plugins,
|
||||
* env vars, and other configuration.
|
||||
*
|
||||
* This defensive check removes invalid hook entries and cleans up empty
|
||||
* event arrays to prevent this. It validates:
|
||||
* - agent hooks require a "prompt" field
|
||||
* - command hooks require a "command" field
|
||||
* - entries must have a valid "hooks" array (non-array/missing is removed)
|
||||
*
|
||||
* @param {object} settings - The settings object (mutated in place)
|
||||
* @returns {object} The same settings object
|
||||
*/
|
||||
function validateHookFields(settings) {
|
||||
if (!settings.hooks || typeof settings.hooks !== 'object') return settings;
|
||||
|
||||
let fixedHooks = false;
|
||||
const emptyKeys = [];
|
||||
|
||||
for (const [eventType, hookEntries] of Object.entries(settings.hooks)) {
|
||||
if (!Array.isArray(hookEntries)) continue;
|
||||
|
||||
// Pass 1: validate each entry, building a new array without mutation
|
||||
const validated = [];
|
||||
for (const entry of hookEntries) {
|
||||
// Entries without a hooks sub-array are structurally invalid — remove them
|
||||
if (!entry.hooks || !Array.isArray(entry.hooks)) {
|
||||
fixedHooks = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Filter invalid hooks within the entry
|
||||
const validHooks = entry.hooks.filter(h => {
|
||||
if (h.type === 'agent' && !h.prompt) {
|
||||
fixedHooks = true;
|
||||
return false;
|
||||
}
|
||||
if (h.type === 'command' && !h.command) {
|
||||
fixedHooks = true;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
// Drop entries whose hooks are now empty
|
||||
if (validHooks.length === 0) {
|
||||
fixedHooks = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build a clean copy instead of mutating the original entry
|
||||
validated.push({ ...entry, hooks: validHooks });
|
||||
}
|
||||
|
||||
settings.hooks[eventType] = validated;
|
||||
|
||||
// Collect empty event arrays for removal (avoid delete during iteration)
|
||||
if (validated.length === 0) {
|
||||
emptyKeys.push(eventType);
|
||||
fixedHooks = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: remove empty event arrays
|
||||
for (const key of emptyKeys) {
|
||||
delete settings.hooks[key];
|
||||
}
|
||||
|
||||
if (fixedHooks) {
|
||||
console.log(` ${green}✓${reset} Fixed invalid hook entries (prevents settings.json schema rejection)`);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall GSD from the specified directory for a specific runtime
|
||||
* Removes only GSD-specific files/directories, preserves user content
|
||||
@@ -4027,7 +4106,7 @@ function install(isGlobal, runtime = 'claude') {
|
||||
// Gemini and Antigravity use AfterTool instead of PostToolUse for post-tool hooks
|
||||
const postToolEvent = (runtime === 'gemini' || runtime === 'antigravity') ? 'AfterTool' : 'PostToolUse';
|
||||
const settingsPath = path.join(targetDir, 'settings.json');
|
||||
const settings = cleanupOrphanedHooks(readSettings(settingsPath));
|
||||
const settings = validateHookFields(cleanupOrphanedHooks(readSettings(settingsPath)));
|
||||
const statuslineCommand = isGlobal
|
||||
? buildHookCommand(targetDir, 'gsd-statusline.js')
|
||||
: 'node ' + dirName + '/hooks/gsd-statusline.js';
|
||||
@@ -4455,6 +4534,7 @@ if (process.env.GSD_TEST_MODE) {
|
||||
copyCommandsAsAntigravitySkills,
|
||||
writeManifest,
|
||||
reportLocalPatches,
|
||||
validateHookFields,
|
||||
};
|
||||
} else {
|
||||
|
||||
|
||||
Reference in New Issue
Block a user