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 <noreply@anthropic.com>
This commit is contained in:
catlog22
2026-01-14 15:07:04 +08:00
parent ac9ba5c7e4
commit d941166d84

View File

@@ -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;
}