The inert stub written when hook consent is not granted was
`export default async () => ({})`. OpenCode v2 validates every plugin
module before running it and requires the default export to be a definition
object with an `id` and an `effect` or `setup` function, so a declined or
runtime-less home install printed the same startup error as the V1 hook
plugin:
PluginModule.LoadError: Plugin must export a default definition with an id
and an effect or setup function.
(cause: SchemaError(Expected object at ["default"]))
Write `export default { id: "ecc-hooks", setup() {} }` instead. It keeps the
inert behaviour (no imports, no module initialization of the replaced
plugin) and satisfies the loader.
Tests that pinned the generated bytes now compare against
getDisabledOpenCodePluginContent() rather than a duplicated literal, and
tests/opencode-plugin-loader-contract.test.js asserts the loader contract on
the generated payload. That test fails on the previous stub
("default export must be an object, got function").
Refs #3537. The same assertion should cover .opencode/plugins/*.ts once a V2
entrypoint lands (#3536, #3224); those plugins are scanned independently in
home installs, so both the barrel and the implementation are loaded.
On native Windows, Claude Code runs each hook as `bash.exe -c "<command>"`
through the Windows command line. That extra unescape layer collapses the
backslashes in the WSL path conversion of the inline plugin-root resolver,
so `.replace(/\\/g,'/')` reaches node as `.replace(/\/g,'/')` and every hook
fails with `SyntaxError: missing ) after argument list`.
Build the backslash separator with String.fromCharCode(92) and split/join
instead of regex escapes, so the resolver contains no backslash. Conversion
behaviour is unchanged. Regenerate every embedded copy (hooks.json,
codex-hooks.json, commands/*.md) and refresh hooks.metadata.json
fingerprints.
Add a test that the resolver and each hook `node -e` program are
backslash-free, and run the shipped SessionStart command through bash with
backslashes collapsed the way Windows does.
Fixes#3746
Integrates #3697 on current main. Prefer the observer project ID, retain a sole legacy ID until the new observer folder exists, and release the ending session lease across same-root aliases. Replace the unsafe live merge hint with read-only project inspection, and validate alias IDs before path use. #3711 independently identified the computed-ID selection gap; its early switch without observer data and unchanged SessionEnd path were not adopted.
Port #3656 onto current main, preserving both the contributor fix and the unrelated current changelog entry. Includes the follow-up glob normalization from 2c11fd74.
Prettier's config searcher also reads .prettierrc.json5, .prettierrc.ts/.mts/.cts and prettier.config.ts/.mts/.cts. resolve-formatter knew none of them, so a project configured only through prettier.config.ts was never formatted by post-edit-format, quality-gate or the Stop batch. config-protection's rc pattern stopped at .yaml/.toml, so an existing .prettierrc.ts or .prettierrc.json5 could be edited freely.
With diff.relative=true, `git diff --name-only` reports paths relative to the working directory, so joining them to the repository root pointed at files that do not exist, and a session in a subdirectory again got no console.log warning. getGitModifiedFiles now runs `git -c diff.relative=false diff --name-only HEAD`. The -c form also works on Git versions older than --no-relative.