Files
juliusbrussee__caveman/bin/lib/platform-paths.js
Julius Brussee 5429b5da47 fix(xplat): stop hooks stalling on Windows; own only our own config entries
Whole-repo review of the surfaces that mutate a user's agent config, plus a
cross-platform sweep focused on Windows.

Hooks (Windows):
- caveman-mode-tracker finished its work in ~50ms then blocked until the host
  killed it at the 5s budget: the 'data' listener REFERENCES the stdin handle,
  so pause() alone keeps the event loop alive until the host closes the write
  end, and on Windows that close lags arbitrarily. Reproduced off-Windows.
  This is the shape of #819. Fixed with process.stdin.unref().
- caveman-activate used readFileSync(0), which blocks INSIDE the read syscall
  until EOF — no deadline can interrupt it. The #729/#833 fix had been applied
  to the tracker and never to its sibling. Rewritten event-driven, acting on
  the first complete payload, with a watchdog so a payload that never arrives
  still activates instead of forfeiting the session.
- hookCommand emitted PowerShell call-operator syntax into settings.json, but
  Claude Code's Windows hook shell is Git Bash, which rejects it outright:
  `syntax error near unexpected token '&'`. Every standalone Windows install
  had two dead hooks (#835). Both emitters now route through one bash-safe
  shape.
- activate now threads the payload cwd, so a checked-in .caveman.json
  defaultMode "off" is honored for the session's directory (#634), and a JSONC
  settings.json no longer suppresses the statusline nudge.

Config ownership:
- src/hooks/uninstall.{sh,ps1} matched hooks by the substring "caveman" and
  filtered whole entries, deleting user hooks that merely mention the word and
  dropping foreign handlers that shared a matcher group (#593). Ported the
  exact-basename matcher from bin/lib/settings.js and switched to filtering the
  inner handler list. Verified against real PowerShell: the old code left zero
  user hooks standing.
- install.{sh,ps1} backed up settings.json unguarded, so a --force reinstall
  overwrote the only pre-caveman copy with the merged file. Back up once.
- OpenClaw overwrote a hand-edited workspace skill with no backup.
- caveman-init appended unfenced blocks to tracked repo files, so they could
  be neither refreshed nor removed; now marker-fenced and written atomically.
- opencode AGENTS.md and OpenClaw SOUL.md were presence-checked, so a ruleset
  edit never reached anyone already installed. Both refresh in place.
- opencode's system-prompt hook had no idempotency guard; it now rewrites its
  line rather than stacking another onto the context window.
- readSettings reports when it stripped comments so both write paths warn and
  point at the backup instead of silently dropping a user's annotations.

Skills: skills/ is auto-discovered wholesale by the plugin loader, so 13
undocumented skills would have shipped into every user's agent on the next
release. Documented them and added a verify_repo check that fails when a
skills/ directory is named in neither README.md nor CLAUDE.md.

CI: a windows-powershell job covering the surface engine-ci never exercised —
the PowerShell installer round trip, and a bash -n gate over every hook command
the installer writes. That gate alone catches #835.

Tests: every fix above is pinned by a test that fails against the old code,
including tests/test_hook_stdin_lifecycle.js, which reproduces the Windows
lagging-close condition on any platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ez7w7BBtKrMZCiZz9ea4cb
2026-08-20 04:14:48 +02:00

46 lines
1.7 KiB
JavaScript

'use strict';
const path = require('path');
function powershellQuote(value) {
return `'${String(value).replace(/'/g, "''")}'`;
}
// RESOLVED (#835): Claude Code runs hook commands through BASH on every
// platform — Git Bash on Windows — unless the hook opts into `"shell":
// "powershell"`. Confirmed from the shipped binary, which carries both
// 'Hook "…" requires bash but Git Bash was not found. … or add
// "shell": "powershell" to this hook'"'"'s config.'
// and the inverse message for a missing PowerShell.
//
// The old win32 branch emitted PowerShell call-operator syntax
// (`& 'C:\\…\\node.exe' 'C:\\…\\hook.js'`), which bash rejects outright:
// bash: syntax error near unexpected token `&'
// That made both caveman hooks fail on every SessionStart and
// UserPromptSubmit for every standalone Windows install.
//
// So both branches emit POSIX quoting. win32 additionally normalizes `\` to
// `/`: Windows Node accepts forward slashes, and it keeps bash from eating
// backslashes as escapes inside the double quotes.
function hookCommand(executable, args, platform = process.platform) {
const normalize = platform === 'win32'
? (value) => String(value).replace(/\\/g, '/')
: (value) => String(value);
return [executable, ...args]
.map(value => `"${normalize(value).replace(/(["\\$`])/g, '\\$1')}"`)
.join(' ');
}
function jetbrainsRoots(home, env = process.env) {
const roots = [
path.join(home, 'Library/Application Support/JetBrains'),
path.join(home, '.config/JetBrains'),
];
for (const key of ['APPDATA', 'LOCALAPPDATA']) {
if (env[key]) roots.push(path.join(env[key], 'JetBrains'));
}
return [...new Set(roots)];
}
module.exports = { hookCommand, jetbrainsRoots, powershellQuote };