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
4 changes: 3 additions & 1 deletion README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,9 @@ Claude Code events:
| `PermissionRequest` / `PreToolUse` with `AskUserQuestion` | 🔴 waiting |
| `SessionEnd` | removed from the board |

Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, and `Stop`.
Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, and `Stop`. Codex
also emits `PermissionRequest` before an existing policy auto-approves the call; a bare event therefore stays
🟢 working, and it only turns 🔴 waiting when the payload explicitly carries `requires_action` / `waiting_for`.

Verify that hooks are writing:

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ Claude Code 註冊這幾個事件,對應到狀態:
| `PermissionRequest` / `PreToolUse` 的 `AskUserQuestion` | 🔴 等你(權限 / 選項需要你決策) |
| `SessionEnd` | 從看板消失 |

Codex 目前註冊 Codex 支援的互動事件:`PreToolUse`、`PermissionRequest`、`Stop`。
Codex 目前註冊 Codex 支援的互動事件:`PreToolUse`、`PermissionRequest`、`Stop`。Codex 的
`PermissionRequest` 也會在既有 policy 自動放行前送出;因此裸事件維持 🟢 工作中,只有 payload
明確標示 `requires_action` / `waiting_for` 時才轉成 🔴 等你。

hook 只對**新開的 session** 生效,所以裝完要重開。確認方法:

Expand Down
5 changes: 3 additions & 2 deletions src/ring/hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@
_HOOK_EVENTS = list(HOOK_EVENTS)

# Codex 的 hooks.json 用跟 Claude 同樣的 PascalCase 事件名,但只支援其中一小撮。
# 保守取有實證可用的:PermissionRequest(→ 🔴 等核可)、PreToolUse(→ 動作/清除)、
# Stop(→ 🟡 回合結束、清掉 waiting)。多裝 Codex 不認的事件有風險,故不照搬 Claude 全套。
# 保守取有實證可用的:PermissionRequest(裸事件只代表權限準備判定;明確需互動才 → 🔴)、
# PreToolUse(→ 動作/清除)、Stop(→ 🟡 回合結束、清掉 waiting)。多裝 Codex 不認的事件
# 有風險,故不照搬 Claude 全套。
_CODEX_HOOK_EVENTS = ["PreToolUse", "PermissionRequest", "Stop"]

# hook command 的 timeout(秒)。給足,因為 notify_backend="agent-hooks" 時權限 modal 會
Expand Down
7 changes: 7 additions & 0 deletions src/ring/hook_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ def normalize(self, data: Mapping[str, Any]) -> NormalizedHookEvent | None:
status = Status.WORKING
elif explicit_requires_action is not None:
status = Status.WAITING if explicit_requires_action else Status.IDLE
elif event == "PermissionRequest" and self.provider == "codex":
# Codex 會在權限「準備判定」時送 PermissionRequest;即使既有 policy
# 隨後自動放行、畫面從未停下來等人,也會經過這個 hook。裸事件因此只能
# 證明 agent 還在處理工具呼叫,不能當成使用者需要回應。若 payload 有
# requires_action / waiting_for 等明確訊號,已由上面的分支判成 WAITING;
# 明確的 requires_action=false 也在上面判成 IDLE,不會落到這裡的 WORKING。
status = Status.WORKING
elif event == "Notification":
status = Status.WAITING if _is_action_required_notification(data) else Status.IDLE
elif event == "PreToolUse":
Expand Down
29 changes: 29 additions & 0 deletions tests/test_hook.py
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,35 @@ def test_codex_provider_writes_qualified_session(monkeypatch: pytest.MonkeyPatch
assert data["status"] == Status.IDLE.value


def test_codex_bare_permission_request_stays_working(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
"""Codex 權限 policy 自動放行時也會送 hook;裸事件不代表真的停下來等人。"""
monkeypatch.setattr(hook, "RING_REGISTRY", tmp_path)
_feed(monkeypatch, {"session_id": "thread-1", "event": "PermissionRequest", "cwd": "/repo"})

assert hook.run_hook(provider="codex") == 0

data = json.loads((tmp_path / "codex:thread-1.json").read_text())
assert data["status"] == Status.WORKING.value


def test_codex_explicit_permission_request_writes_waiting(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(hook, "RING_REGISTRY", tmp_path)
_feed(
monkeypatch,
{
"session_id": "thread-1",
"event": "PermissionRequest",
"cwd": "/repo",
"requires_action": True,
},
)

assert hook.run_hook(provider="codex") == 0

data = json.loads((tmp_path / "codex:thread-1.json").read_text())
assert data["status"] == Status.WAITING.value


def test_payload_provider_overrides_default(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setattr(hook, "RING_REGISTRY", tmp_path)
_feed(monkeypatch, {"provider": "codex", "session_id": "thread-2", "event": "UserPromptSubmit", "cwd": "/repo"})
Expand Down
Loading