fix(kvb-org-organize): harden the council hooks against sandboxed agents
- Move the council config to ~/.claude/agents-council/config.json, which the sandbox cannot write; the installer adds Edit(~/.claude/agents-council/**) to permissions.deny. - Run every kvb hook as /usr/bin/python3 -I and rewrite old bare python3 entries. - Refuse a council root that is a symlink, another uid's, or group or other writable, in watch, queue and radio. - Run worktree_cleanup entries as argv without a shell in /, only for compose project names. - queue block: tokenize every Bash command like bash (quotes, word-start comments, line continuations), and hold each held command to the Bash deny and ask rules of the managed, user and project settings; refuse a hold it cannot parse. - radio: deliver fields on one line of at most 1000 characters, at most 10 rows. Operator step after install: move ~/.config/agents-council/config.json to ~/.claude/agents-council/config.json. MEDUSA acceptances (kvb-think-criticize 2026-11-06, skeptics A and B): ACP-SCAN-028 load_config (config outside the sandbox, Docker residual 093); CODEGEN-SCAN-004 and LLM06 run_cleanup and worktrees_command shell strings (operator config only); SAST-PATH-001 queue hold Popen (the caller's own command in its sandbox); PA-133 radio hook context (peer rows, clipped); JB-SCAN-641/616/2979 clip and MM-SCAN-058 replace (name matches). 🎯 Fix: 90 % 💥 Break: 5 % 💸 Cost: 100 % ⏱️ Time: 100 % 🛡️ Security: 100 % 💰 Value: ? 🏆 Score: 86 🚦 Gate: ✅ 90/5, cost and time <= 100, security 100 % Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6985c3a27e
commit
3843821674
11 files changed
+414
-54
No files matched your search
@@ -60,6 +60,10 @@ for (const [plugin, marketplace] of Object.entries(RETIRED_PLUGINS)) {
|
||||
}
|
||||
data.env = data.env || {};
|
||||
if (!data.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW) data.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW = "500000";
|
||||
const PYTHON = "/usr/bin/python3 -I";
|
||||
const COUNCIL_CONFIG_DENY = "Edit(~/.claude/agents-council/**)";
|
||||
data.permissions = data.permissions || {};
|
||||
data.permissions.deny = [...new Set([...(data.permissions.deny || []), COUNCIL_CONFIG_DENY])];
|
||||
|
||||
const pluginRepos = new Set(Object.values(MARKETPLACES).map((entry) => entry.source.repo));
|
||||
const lock = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "skills-lock.json"), "utf8"));
|
||||
@@ -87,6 +91,7 @@ for (const entries of Object.values(data.hooks)) {
|
||||
for (const [from, to] of Object.entries(RENAMED)) {
|
||||
hook.command = hook.command.replace(`/skills/${from}`, `/skills/${to}`);
|
||||
}
|
||||
if (hook.command.includes("/skills/kvb-")) hook.command = hook.command.replace(/(^|&& )python3 /, `$1${PYTHON} `);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,7 +110,7 @@ if (!JSON.stringify(data.hooks.PreToolUse).includes("kvb-org-organize/scripts/co
|
||||
const council = path.join(path.dirname(settingsPath), "skills", "kvb-org-organize", "scripts", "council.py");
|
||||
data.hooks.PreToolUse.push({
|
||||
matcher: "Edit|Write|MultiEdit|NotebookEdit",
|
||||
hooks: [{ type: "command", command: `python3 ${JSON.stringify(council)} gate` }],
|
||||
hooks: [{ type: "command", command: `${PYTHON} ${JSON.stringify(council)} gate` }],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,7 +133,7 @@ for (const [script, event, matcher, sub, extra] of SCRIPT_HOOKS) {
|
||||
if (registered) continue;
|
||||
const file = JSON.stringify(path.join(path.dirname(settingsPath), "skills", ...script.split("/")));
|
||||
// Trip-wire: python3 on a missing file exits 2, which blocks every prompt or tool call; the test makes it exit 1.
|
||||
const command = `[ -f ${file} ] && python3 ${file} ${sub}`;
|
||||
const command = `[ -f ${file} ] && ${PYTHON} ${file} ${sub}`;
|
||||
const entry = { hooks: [{ type: "command", command, ...extra }] };
|
||||
data.hooks[event].push(matcher ? { matcher, ...entry } : entry);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user