0.6.0 removed the working-directory fallback, which was right, but it left every non-git working surface with nothing to derive from. The read path declines silently by design - the hook must never fail a session - so such a surface simply stops seeing its inbox: no error, no exit code, nothing. That is the same loss-looks-like-normal shape 0.6.0 set out to remove, reintroduced one layer up. The hook now forwards CLAUDE_COORD_REPO verbatim as --repo. This is not the fallback returning, and the distinction is the whole point: the fallback GUESSED a name from wherever the session happened to stand, while a declaration is written down in that directory's settings, readable back, and deletable. Identity stays a choice someone made. Forwarding as --repo rather than reimplementing anything means it inherits every engine rule, including that an explicit override never claims <repo>/.origin - otherwise a surface borrowing a name would steal the claim from the checkout that owns it. Both halves are pinned by tests, and the shared test helper now deletes CLAUDE_COORD_REPO unless a test asks for it, so a value set globally later cannot silently satisfy the tests that prove the hook resolves nothing on its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U6EixQo6hpoRCVtiAXdnFs
50 lines
2.3 KiB
JavaScript
50 lines
2.3 KiB
JavaScript
#!/usr/bin/env node
|
|
// coord - SessionStart hook: inject this repo's pending coordination inbox
|
|
// (directed messages + unseen broadcasts) as additionalContext.
|
|
//
|
|
// Thin Node wrapper (marketplace convention: hooks are .mjs) around the bash
|
|
// engine scripts/coord-inbox.sh, which owns the mailbox semantics and is
|
|
// covered by scripts/coord-selftest.sh. Zero dependencies. Always exits 0 -
|
|
// a broken mailbox must never block a session.
|
|
|
|
import { execFileSync } from 'node:child_process';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
function emit(context) {
|
|
const out = { continue: true };
|
|
if (context) {
|
|
out.hookSpecificOutput = { hookEventName: 'SessionStart', additionalContext: context };
|
|
}
|
|
process.stdout.write(JSON.stringify(out) + '\n');
|
|
}
|
|
|
|
try {
|
|
const pluginRoot = process.env.CLAUDE_PLUGIN_ROOT
|
|
|| join(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
|
|
// No identity resolution here. This used to resolve the repo itself and fall
|
|
// back to process.cwd(), which made it a fourth independent copy of the
|
|
// identity rule - and the only one that runs in production, so the engine's
|
|
// guards were bypassed exactly where they mattered. The engine runs in this
|
|
// same cwd and derives the identity from git alone.
|
|
//
|
|
// CLAUDE_COORD_REPO is the one exception, and it is a DECLARATION rather than
|
|
// a derivation: a working surface that is not a git repo (~/repos, $HOME) has
|
|
// nothing to derive from, so the read path declines silently and the surface
|
|
// loses its injection with no error - loss wearing the shape of normal. The
|
|
// operator sets this in that directory's settings to say which mailbox the
|
|
// surface owns. It is not the pwd fallback returning: the fallback guessed,
|
|
// this is written down, readable back, and deletable. Forwarded verbatim as
|
|
// --repo, so it inherits the engine's rules - including that an explicit
|
|
// override never claims .origin. Boundary rule holds: no mailbox logic here.
|
|
const declared = process.env.CLAUDE_COORD_REPO;
|
|
const script = join(pluginRoot, 'scripts', 'coord-inbox.sh');
|
|
const inbox = execFileSync('bash',
|
|
declared ? [script, '--repo', declared] : [script],
|
|
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' });
|
|
|
|
emit(inbox.trim() ? '== Repo coordination (unread messages) ==\n' + inbox : '');
|
|
} catch {
|
|
emit('');
|
|
}
|