Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .agents/hooks/rules/related_test_extensions.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"tests/agents/hooks/test_check_selection.py"
],
"agents/hooks/rules/secret_path_policy.json": [
"tests/agents/hooks/test_secret_path_policy.py",
"tests/agents/codex/test_hook_adapter_cli.py",
"tests/agents/hooks/test_verification_rule.py"
],
Expand Down Expand Up @@ -36,6 +37,7 @@
],
"agents/claude/settings.json": [
"tests/agents/claude/test_settings.py",
"tests/agents/hooks/test_sandbox_escape.py",
"tests/agents/hooks/test_allowed_command.py",
"tests/agents/hooks/test_command_cli.py",
"tests/agents/test_notification.py"
Expand All @@ -50,6 +52,7 @@
"tests/nix/configuration.bats"
],
"agents/hooks.json": [
"tests/agents/hooks/test_sandbox_escape.py",
"tests/agents/test_notification.py",
"tests/agents/hooks/test_verification_rule.py",
"tests/agents/test_python_runtime.py",
Expand Down Expand Up @@ -331,5 +334,14 @@
"agents/pi/hooks/hook_adapter.py": [
"tests/agents/pi/test_pi_hook_adapter.py",
"tests/agents/test_hook_generation.py"
],
"agents/hooks/lib/provider_adapter.py": [
"tests/agents/test_provider_adapters.py",
"tests/agents/hooks/test_hook_boundary.py",
"tests/agents/codex/test_hook_adapter_cli.py",
"tests/agents/devin/test_hook_adapter.py",
"tests/agents/hermes/test_hermes_hook_adapter.py",
"tests/agents/pi/test_pi_hook_adapter.py",
"tests/agents/test_python_runtime.py"
]
}
5 changes: 4 additions & 1 deletion agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ Existing project style takes precedence over these rules.
- Keep classes and modules single-purpose, cohesive, loosely coupled, and minimally public.
- Name classes by responsibility; order methods public-to-private; use DTOs at component boundaries.
- Keep domain-specific enums and exception classes near their owning class; use one exception class per domain failure concept unless it is shared across modules.
- Keep functions small and single-purpose: prefer 0-3 arguments, one abstraction level, 2-4 lines when practical, short variable lifetimes, guard clauses, and at most one indentation level; split only when the extracted name clarifies intent.
- Keep functions single-purpose at one abstraction level: prefer 0-3 arguments, short variable lifetimes, and guard clauses over nested conditionals; keep nesting to about two levels.
- Extract a function when its name states intent the inline code does not; do not split merely to shorten a function.
- Split duplicated logic, control structures, mixed responsibilities, and command/query behavior into named functions or objects.
- Name one concept with one word; name command functions for their side effects and query functions for the value they return.
- Let name length match scope size; include units, trust/safety attributes, and boolean prefixes where they clarify meaning.
Expand All @@ -38,3 +39,5 @@ Existing project style takes precedence over these rules.
- Prefer Collection Objects for domain collections with invariants; do not expose mutable raw collections
- Use Entities only when stable identity matters across state changes
- Use classification objects/enums when categories or state transitions have domain rules
- Parse raw input into constrained types once at the boundary; do not re-validate the same value downstream
- Make invalid states unrepresentable where the language allows: return expected failures as values (Result or an equivalent) and handle sum types exhaustively. Types do not replace tests for business rules, ordering, side effects, or integration
16 changes: 12 additions & 4 deletions agents/claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"GIT_CONFIG_VALUE_0": "false"
},
"permissions": {
"defaultMode": "auto"
"defaultMode": "auto",
"ask": [
"Bash(dangerouslyDisableSandbox:true)"
]
},
"model": "fable",
"preferredNotifChannel": "auto",
Expand Down Expand Up @@ -48,7 +51,10 @@
"allowUnsandboxedCommands": true,
"filesystem": {
"allowWrite": [
"$HOME/.cache/nix"
"~/.cache/nix",
"~/.cache/uv",
"~/Library/Caches/dprint",
"~/.local/state/agent-harness"
]
},
"network": {
Expand All @@ -57,8 +63,10 @@
]
},
"excludedCommands": [
"docker",
"watchman"
"docker *",
"gh *",
"herdr *",
"watchman *"
]
},
"effortLevel": "xhigh",
Expand Down
2 changes: 1 addition & 1 deletion agents/codex/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ enabled = false

[features]
hooks = true
remote_connections = true
external_migration = true

[features.multi_agent_v2]
enabled = true
min_wait_timeout_ms = 120000
default_wait_timeout_ms = 120000
147 changes: 10 additions & 137 deletions agents/codex/hooks/hook_adapter.py
Original file line number Diff line number Diff line change
@@ -1,152 +1,25 @@
#!/usr/bin/env -S python3 -IB
"""Translate Codex hook payloads at the boundary to shared hook implementations."""

import json
import os
from pathlib import Path
import sys

SCRIPT = Path(__file__).resolve()
COMMON = SCRIPT.parents[2] / (".claude/hooks" if SCRIPT.parent.parent.name == ".codex" else "hooks")
# Deployed adapters live in ~/.<provider>/hooks beside ~/.claude/hooks; the source tree keeps agents/hooks.
COMMON = SCRIPT.parents[2] / (".claude/hooks" if SCRIPT.parent.parent.name.startswith(".") else "hooks")
sys.path.insert(0, str(COMMON))
sys.path.insert(0, str(COMMON / "lib"))

import audit_log
import guard_command
import guard_git
import guard_file
import hook_input
import lint_format
import patch_input
import secret_path_policy
import shell_syntax
import provider_adapter

EXTENSIONS = lint_format.EXTENSIONS


def shared_directory() -> Path:
return Path(os.environ.get("AGENT_HARNESS_ROOT", str(Path.home()))) / ".claude/hooks"


def translated(payload: dict, tool: str, values: dict) -> dict:
return {
"tool_name": tool,
"tool_input": values,
"session_id": payload.get("session_id") or "",
"cwd": payload.get("cwd") or "",
"provider": "codex",
}


def lint(payload: dict) -> int:
"""Report shared quality diagnostics using Codex's PostToolUse JSON contract."""
lint_format.emit(lint_format.diagnostics(payload))
return 0


def harness(payload: dict) -> int:
values = payload.get("tool_input") or {}
body = hook_input.patch_body(values, payload.get("tool_name"))
for name in patch_input.paths(body):
absolute = name if name.startswith(("/", "~/")) else str(Path.cwd() / name)
if status := guard_file.check(
"harness", translated(payload, "apply_patch", {"file_path": absolute}), shared_directory()
):
return status
return 0


def check_paths(mode: str, payload: dict) -> int:
policy = Path(
os.environ.get("AGENT_SECRET_PATH_POLICY", str(shared_directory() / "rules/secret_path_policy.json"))
)
rules = secret_path_policy.load(policy)
values = payload.get("tool_input") or {}
command = values.get("command") or values.get("cmd") or ""
if mode == "command":
try:
commands = shell_syntax.parse(command)
except shell_syntax.UnsupportedSyntax as error:
raise ValueError(f"{error}\n\nCommand: {shell_syntax.command_preview(command)}") from error
candidates = [word for item in commands for word in (*item.arguments, *item.redirections)]
candidates += [word.partition("=")[2] for word in candidates if "=" in word]
else:
candidates = [
values.get("file_path") or values.get("path") or "",
*patch_input.paths(hook_input.patch_body(values, payload.get("tool_name"))),
]
for value in candidates:
if rule := secret_path_policy.blocked_rule(value, rules):
audit_log.blocked(
"Bash" if mode == "command" else "apply_patch",
command if mode == "command" else value,
f"{rule['reason']}: {value}",
"adapt_guard_secret_paths.sh",
payload.get("session_id") or "",
payload=payload,
targets=() if mode == "command" else (value,),
)
raise ValueError(
f"secret path policy matched.\n\nPath: {value}\nPattern: {rule['pattern']}\n\nWhy:\n {rule['reason']}"
)
return 0


def dispatch(kind: str, arguments: list[str], payload: dict) -> int:
values = payload.get("tool_input") or {}
if kind == "shell":
command = translated(payload, "Bash", {"command": values.get("command") or values.get("cmd") or ""})
if arguments[0] in {"allowed", "forbidden"}:
return guard_command.check(arguments[0], command, shared_directory())
if arguments[0] == "git":
return guard_git.check(command)
return guard_file.check("commit", command, shared_directory())
if kind == "lint":
return lint(payload)
if kind == "harness":
return harness(payload)
if kind == "paths":
return check_paths(arguments[0], payload)
if arguments[0] == "prompt":
return guard_file.check("prompt", payload, shared_directory())
return guard_file.check(
"write",
translated(
payload,
"Edit",
{"content": patch_input.added_text(hook_input.patch_body(values, payload.get("tool_name")))},
),
shared_directory(),
)


def main(arguments: list[str]) -> int:
kind = arguments[0] if arguments else ""
rest = arguments[1:]
valid = (
(kind in {"lint", "harness"} and not rest)
or (kind == "shell" and len(rest) == 1 and rest[0] in {"allowed", "forbidden", "git", "commit"})
or (
kind in {"paths", "content"}
and len(rest) == 1
and rest[0] in ({"command", "patch"} if kind == "paths" else {"prompt", "apply-patch"})
)
ADAPTER = provider_adapter.Adapter(
provider_adapter.Profile(
name="codex",
content_modes=("prompt", "apply-patch"),
cwd_fallback=provider_adapter.no_cwd,
)
if not valid or any(value in {"-h", "--help"} for value in rest):
print(
"Usage: hook_adapter.py <shell allowed/forbidden/git/commit|lint|harness|paths command/patch|content prompt/apply-patch>",
file=sys.stderr,
)
return 1
try:
payload = json.load(sys.stdin)
if payload.get("cwd"):
os.chdir(payload["cwd"])
return dispatch(kind, rest, payload)
except (OSError, ValueError, TypeError, AttributeError) as error:
print(f"BLOCKED: {error}", file=sys.stderr)
return 2
)


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
sys.exit(ADAPTER.main(sys.argv[1:]))
34 changes: 22 additions & 12 deletions agents/command_permissions.json
Original file line number Diff line number Diff line change
Expand Up @@ -1019,32 +1019,27 @@
{
"decision": "allow",
"prefix": ["docker", "ps"],
"justification": "Allow read-only container inspection and image builds."
"justification": "Allow read-only container inspection."
},
{
"decision": "allow",
"prefix": ["docker", "images"],
"justification": "Allow read-only container inspection and image builds."
"justification": "Allow read-only container inspection."
},
{
"decision": "allow",
"prefix": ["docker", "logs"],
"justification": "Allow read-only container inspection and image builds."
},
{
"decision": "allow",
"prefix": ["docker", "build"],
"justification": "Allow read-only container inspection and image builds."
"justification": "Allow read-only container inspection."
},
{
"decision": "allow",
"prefix": ["docker", "compose", "ps"],
"justification": "Allow read-only container inspection and image builds."
"justification": "Allow read-only container inspection."
},
{
"decision": "allow",
"prefix": ["docker", "compose", "logs"],
"justification": "Allow read-only container inspection and image builds."
"justification": "Allow read-only container inspection."
},
{
"decision": "allow",
Expand Down Expand Up @@ -1328,7 +1323,12 @@
},
{
"decision": "ask",
"prefix": ["herdr", "pane", "send"],
"prefix": ["herdr", "pane", "send-text"],
"justification": "Ask before controlling other panes; their commands run outside this policy."
},
{
"decision": "ask",
"prefix": ["herdr", "pane", "send-keys"],
"justification": "Ask before controlling other panes; their commands run outside this policy."
},
{
Expand All @@ -1348,7 +1348,12 @@
},
{
"decision": "ask",
"prefix": ["herdr", "agent", "send"],
"prefix": ["herdr", "agent", "send-keys"],
"justification": "Ask before controlling other panes; their commands run outside this policy."
},
{
"decision": "ask",
"prefix": ["herdr", "agent", "prompt"],
"justification": "Ask before controlling other panes; their commands run outside this policy."
},
{
Expand Down Expand Up @@ -1381,6 +1386,11 @@
"prefix": ["esa", "post", "update"],
"justification": "Ask before publishing or triggering work that is visible to others."
},
{
"decision": "ask",
"prefix": ["docker", "build"],
"justification": "Ask before building images; build steps run arbitrary commands outside the sandbox."
},
{
"decision": "ask",
"prefix": ["docker", "run"],
Expand Down
Loading
Loading