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
@@ -217,6 +217,29 @@ class TestSettingsRegistration(unittest.TestCase):
|
||||
self.settings.write_text(json.dumps({"env": {"CLAUDE_CODE_AUTO_COMPACT_WINDOW": "300000"}}), encoding="utf-8")
|
||||
self.assertEqual(self._patch()["env"]["CLAUDE_CODE_AUTO_COMPACT_WINDOW"], "300000")
|
||||
|
||||
def test_hooks_run_the_system_python_isolated_and_old_entries_move_to_it(self):
|
||||
old = f'[ -f "{self.tmp.name}/skills/kvb-org-queue/scripts/queue.py" ] && python3 "{self.tmp.name}/skills/kvb-org-queue/scripts/queue.py" block'
|
||||
self.settings.write_text(
|
||||
json.dumps({"hooks": {"PreToolUse": [{"matcher": "Bash", "hooks": [{"type": "command", "command": old}]}]}}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
data = self._patch()
|
||||
commands = [h["command"] for entries in data["hooks"].values() for e in entries for h in e.get("hooks", [])]
|
||||
python = [c for c in commands if ".py" in c]
|
||||
self.assertTrue(python)
|
||||
for command in python:
|
||||
with self.subTest(command=command):
|
||||
self.assertIn("/usr/bin/python3 -I ", command)
|
||||
self.assertNotRegex(command, r"(^|&& )python3 ")
|
||||
self.assertEqual(len(self._hooks(data, "PreToolUse", "kvb-org-queue/scripts/queue.py")), 1)
|
||||
|
||||
def test_council_config_deny_is_added_once_next_to_existing_rules(self):
|
||||
self.settings.write_text(json.dumps({"permissions": {"deny": ["Bash(rm -rf:*)"]}}), encoding="utf-8")
|
||||
self._patch()
|
||||
self.assertEqual(
|
||||
self._patch()["permissions"]["deny"], ["Bash(rm -rf:*)", "Edit(~/.claude/agents-council/**)"]
|
||||
)
|
||||
|
||||
def test_plugin_skills_are_not_copied_twice(self):
|
||||
skills = Path(self.tmp.name) / "skills"
|
||||
for name in ("caveman", "ponytail-review", "cavecrew", "archify", "kvb-dream"):
|
||||
|
||||
Reference in new issue
Block a user