fix(cli-registry): log why clis.json was ignored, and say 0600 when that is the rule

The loader refuses a `clis.json` with any group/world permission bit, read bits
included, so a file created with a normal umask (0644) is ignored. That is a
defensible posture for a file that chooses the binaries Codeman spawns, but two
things around it made the override feature look dead: the warning said
"group/world-writable", which a 0644 file is not, and `LoadResult.warnings` was
returned to a caller nobody wired up, so nothing anywhere printed it. A user
following the docs got silence.

The message now names the rule and the command that satisfies it, the loader
logs every warning once on first load (the result is memoized, so once per
process), the module header stops claiming that nothing ever writes (the
quarantine rename of a malformed file is a write, on first use) and the
registry doc gains a short section on the override file with the 0600
requirement in it. Whether the check should relax to writable bits only is a
separate decision; this keeps the shipped behaviour and makes it visible.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qg6bcATm1pNNY4kQWGwzgu
This commit is contained in:
Codeman maintainer
2026-09-04 13:51:20 +02:00
parent 2e0129f1f8
commit a81e87f440
2 changed files with 21 additions and 4 deletions
+4
View File
@@ -16,6 +16,10 @@ Every run mode Codeman can launch — Claude Code, Terminal/Shell, OpenCode, Cod
`src/session-cli-registry-bridge.ts` maps the legacy per-mode option bag onto the engine, and `src/utils/cli-resolver.ts` / `src/utils/cli-launcher.ts` do registry-driven binary resolution and launcher-profile dispatch.
## The override file
`~/.codeman/clis.json` (instance-scoped through `dataPath()`) holds overrides and custom entries only, never a copy of the stock catalog: `{ "clis": { "<id>": { ...partial entry... } } }`. Objects merge key-wise onto the stock entry, arrays replace wholesale. **The file must be mode 0600**; the loader refuses any group/world permission bit, read bits included, so a file created with a normal umask (0644) is ignored until you `chmod 600` it. Every reason a file was ignored or an entry dropped is logged once, prefixed `[cli-registry]`, on the first load. A stock entry whose override fails validation falls back to the shipped definition; a custom entry that fails is dropped. The file is read once per process and re-read only on restart.
## The shape of an entry
```ts
+17 -4
View File
@@ -18,6 +18,14 @@
* so there is nothing to persist, and it means importing the registry — which
* `src/web/schemas.ts` does, transitively, just to validate a request — performs no
* filesystem writes. A `seededStockIds` ratchet belongs with the write API that needs it.
* The one exception is the quarantine RENAME of a file that fails to parse (see
* `readRegistryFile`), which happens on first use rather than at import.
*
* ⚠️ The file must be mode 0600. `isUnsafePermissions` refuses ANY group/world bit, read
* bits included, so a file created with a normal umask (0644) is ignored. Every reason the
* file was ignored, or an entry in it dropped, is logged ONCE on first load: the warnings
* used to be returned to a caller that nobody wired up, so a normally-created file was
* ignored with no feedback anywhere (found reviewing #347).
*
* @module config/cli-registry/registry
*/
@@ -74,9 +82,9 @@ export interface LoadResult {
}
/**
* Refuse a group/world-writable registry file — same posture as the ssh-key discipline.
* This file selects the binaries Codeman spawns, so a writable one is a way to redirect
* every session.
* Refuse a registry file with any group/world permission bit — same posture as the ssh-key
* discipline, so 0600 is the only accepted mode. This file selects the binaries Codeman
* spawns, so a writable one is a way to redirect every session.
*
* POSIX only: Windows has no meaningful group/world bits on NTFS (Node reports every file
* as mode 0o666 there regardless of its actual ACL), so this check would flag every file on
@@ -96,7 +104,9 @@ function isUnsafePermissions(path: string): boolean {
function readRegistryFile(path: string, warnings: string[]): CliRegistryFile | null {
if (!existsSync(path)) return null;
if (isUnsafePermissions(path)) {
warnings.push(`${path} is group/world-writable; ignoring it and falling back to stock CLIs.`);
warnings.push(
`${path} must be mode 0600 (no group/world permission bits; run \`chmod 600 ${path}\`); ignoring it and falling back to stock CLIs.`
);
return null;
}
let raw: string;
@@ -181,6 +191,9 @@ export function loadCliRegistry(): LoadResult {
const warnings: string[] = [];
const existing = readRegistryFile(filePath(), warnings);
cache = resolveRegistry(STOCK_CLIS, existing, warnings);
// Once per process (the result is memoized): silence here is what made a 0644 file look
// like "the override feature does nothing".
for (const warning of warnings) console.warn(`[cli-registry] ${warning}`);
return cache;
}