From d941166d84b6155404e4c90dd2343f99e62245a6 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 14 Jan 2026 15:07:04 +0800 Subject: [PATCH] fix: use single quotes for bash -c script to avoid jq escaping issues Problem: When generating hook configurations, the convertToClaudeCodeFormat function was using double quotes to wrap bash -c script arguments. This caused complex escaping issues with jq commands inside, leading to parse errors like "jq: error: syntax error, unexpected end of file". Solution: For bash -c commands, now use single quotes to wrap the script argument. Single quotes prevent shell expansion, so internal double quotes (like those used in jq patterns) work naturally without excessive escaping. If the script contains single quotes, they are properly escaped using the '\'' pattern (close quote, escaped quote, reopen quote). Fixes: https://github.com/catlog22/Claude-Code-Workflow/issues/73 Co-Authored-By: Claude Opus 4.5 --- .../dashboard-js/components/hook-manager.js | 51 ++++++++++++++----- 1 file changed, 38 insertions(+), 13 deletions(-) diff --git a/ccw/src/templates/dashboard-js/components/hook-manager.js b/ccw/src/templates/dashboard-js/components/hook-manager.js index be2a2cbb..91aacf72 100644 --- a/ccw/src/templates/dashboard-js/components/hook-manager.js +++ b/ccw/src/templates/dashboard-js/components/hook-manager.js @@ -359,48 +359,73 @@ async function loadAvailableSkills() { * Convert internal hook format to Claude Code format * Internal: { command, args, matcher, timeout } * Claude Code: { matcher, hooks: [{ type: "command", command: "...", timeout }] } + * + * IMPORTANT: For bash -c commands, use single quotes to wrap the script argument + * to avoid complex escaping issues with jq commands inside. + * See: https://github.com/catlog22/Claude-Code-Workflow/issues/73 */ function convertToClaudeCodeFormat(hookData) { // If already in correct format, return as-is if (hookData.hooks && Array.isArray(hookData.hooks)) { return hookData; } - + // Build command string from command + args let commandStr = hookData.command || ''; if (hookData.args && Array.isArray(hookData.args)) { - // Join args, properly quoting if needed - const quotedArgs = hookData.args.map(arg => { - if (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) { - return `"${arg.replace(/"/g, '\\"')}"`; + // Special handling for bash -c commands: use single quotes for the script + // This avoids complex escaping issues with jq and other shell commands + if (commandStr === 'bash' && hookData.args.length >= 2 && hookData.args[0] === '-c') { + // Use single quotes for bash -c script argument + // Single quotes prevent shell expansion, so internal double quotes work naturally + const script = hookData.args[1]; + // Escape single quotes within the script: ' -> '\'' + const escapedScript = script.replace(/'/g, "'\\''"); + commandStr = `bash -c '${escapedScript}'`; + // Handle any additional args after the script + if (hookData.args.length > 2) { + const additionalArgs = hookData.args.slice(2).map(arg => { + if (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) { + return `"${arg.replace(/"/g, '\\"')}"`; + } + return arg; + }); + commandStr += ' ' + additionalArgs.join(' '); } - return arg; - }); - commandStr = `${commandStr} ${quotedArgs.join(' ')}`.trim(); + } else { + // Default handling for other commands + const quotedArgs = hookData.args.map(arg => { + if (arg.includes(' ') && !arg.startsWith('"') && !arg.startsWith("'")) { + return `"${arg.replace(/"/g, '\\"')}"`; + } + return arg; + }); + commandStr = `${commandStr} ${quotedArgs.join(' ')}`.trim(); + } } - + const converted = { hooks: [{ type: 'command', command: commandStr }] }; - + // Add matcher if present (not needed for UserPromptSubmit, Stop, etc.) if (hookData.matcher) { converted.matcher = hookData.matcher; } - + // Add timeout if present (in seconds for Claude Code) if (hookData.timeout) { converted.hooks[0].timeout = Math.ceil(hookData.timeout / 1000); } - + // Preserve replaceIndex for updates if (hookData.replaceIndex !== undefined) { converted.replaceIndex = hookData.replaceIndex; } - + return converted; }