thedotmack/claude-mem · critical · Error

Hand-edited shell string detected in ${filePath} (${dottedPa

Error message

Hand-edited shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. Regenerate via `node scripts/build-hooks.js --write-shell-templates` after an intentional generator change.

What it means

Same Rule A canonicality guard as [2], but for hook entries of kind other than 'mcp'. For each dottedPath (event.groupIdx.hookIdx) in the manifest, it resolves the hook via hookEntryByPath and compares entry.command to the expected command string from the generator. A mismatch on the Unix/macOS command field throws this. Fix is identical: regenerate or revert.

Source

Thrown at scripts/build-hooks.js:195

      const actual = parsed.mcpServers?.['mcp-search']?.args?.[1] ?? '';
      if (actual !== spec.command) {
        if (!writeMode) {
          throw new Error(
            `Hand-edited shell string detected in ${filePath} (mcp-search). It no longer matches src/build/hook-shell-template.ts. ` +
            `Regenerate via \`node scripts/build-hooks.js --write-shell-templates\` after an intentional generator change.`
          );
        }
        parsed.mcpServers['mcp-search'].args[1] = spec.command;
        dirty = true;
      }
    } else {
      for (const [dottedPath, expected] of Object.entries(spec.commands)) {
        const entry = hookEntryByPath(parsed, dottedPath);
        const expectedCommand = typeof expected === 'string' ? expected : expected.command;
        const actual = entry?.command ?? null;
        if (actual !== expectedCommand) {
          if (!writeMode || !entry) {
            throw new Error(
              `Hand-edited shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` +
              `Regenerate via \`node scripts/build-hooks.js --write-shell-templates\` after an intentional generator change.`
            );
          }
          entry.command = expectedCommand;
          dirty = true;
        }
        if (typeof expected !== 'string') {
          const actualWindows = entry?.commandWindows ?? null;
          if (actualWindows !== expected.commandWindows) {
            if (!writeMode || !entry) {
              throw new Error(
                `Hand-edited Windows shell string detected in ${filePath} (${dottedPath}). It no longer matches src/build/hook-shell-template.ts. ` +
                `Regenerate via \`node scripts/build-hooks.js --write-shell-templates\` after an intentional generator change.`
              );
            }
            entry.commandWindows = expected.commandWindows;
            dirty = true;

View on GitHub (pinned to d768ba3643)

Solutions

  1. Run node scripts/build-hooks.js --write-shell-templates to rewrite entry.command from the generator.
  2. If writeMode reports the entry is missing (!entry branch), restore the hook entry in the JSON first (it must exist for the regenerator to populate), then re-run.
  3. If unintentional, git checkout -- <filePath> to restore the canonical committed launcher.
  4. Re-run the build to confirm '✓ Rule A shell templates match the canonical generator'.

Example fix

# before: hook command drifted from generator
"command": "/old/launcher/path script.js"

node scripts/build-hooks.js --write-shell-templates

# after: command rewritten to canonical src/build/hook-shell-template.ts output
Defensive patterns

Strategy: validation

Validate before calling

// Pre-commit verification: run the same verifier the build runs.
// node scripts/build-hooks.js  -> exits non-zero if any hook command drifted.

Type guard

function hasCommand(entry): entry is { command: string } {
  return entry != null && typeof entry.command === 'string';
}

Try / catch

// Build-time only. On drift either:
//   node scripts/build-hooks.js --write-shell-templates   (accept generator)
//   git checkout -- <file>                                 (reject hand edit)
// Do not suppress in CI.

Prevention

When it happens

Trigger: A committed hooks JSON (e.g. plugin/hooks/settings.json, .claude/settings.json) has a hooks[event][group].hooks[i].command string that differs from what buildShellCommand produces for that hook. Triggered during build verification. Also thrown when writeMode is on but the hook entry is missing entirely (entry is null), because there's nothing to write into.

Common situations: Hand-editing a hook command to point at a different script. Adding a new hook in the manifest generator but forgetting --write-shell-templates. A hook entry deleted from the JSON so hookEntryByPath returns null while the manifest still lists its dottedPath.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/c5626d62f17832d3. Report an issue: GitHub.