diff --git a/cmd/onecli/run.go b/cmd/onecli/run.go index a1672a3..3b7609a 100644 --- a/cmd/onecli/run.go +++ b/cmd/onecli/run.go @@ -457,7 +457,7 @@ type agentSpec struct { agentName string baseDir string // home-relative config dir (skills/hooks/plugins live here) configDir string // VS Code-style app dir name; non-empty enables Electron proxy-settings injection. - skipHook bool // true for agents that don't support Claude Code-style UserPromptSubmit hooks. + skipHook bool // true when the gateway hook shouldn't be registered — either the agent has no Claude Code-style hooks (Hermes), or it renders injected hook context visibly in the transcript (Codex), where the auto-loaded onecli-gateway skill carries the same guidance without the noise. pluginGateway bool // true for agents that load the transform_tool_result recovery plugin (e.g. Hermes). dockerSandbox bool // true for agents that run tools in a Docker sandbox needing TERMINAL_DOCKER_* injection. needsAnthropicKey bool // true for agents that refuse to start without a provider key in the env (e.g. OpenClaw); a placeholder is ensured, the gateway swaps in the real key per request. @@ -472,7 +472,11 @@ var supportedAgents = []struct { }{ {[]string{"claude"}, agentSpec{agentName: "Claude Code", baseDir: ".claude"}}, {[]string{"cursor", "agent"}, agentSpec{agentName: "Cursor", baseDir: ".cursor", configDir: "Cursor"}}, - {[]string{"codex"}, agentSpec{agentName: "Codex", baseDir: ".agents", nativeProxyConfig: ".codex", hooksFile: ".codex/hooks.json"}}, + // Codex skips the hook: it echoes injected hook context into the + // transcript (Claude injects it silently), so the hook is pure noise + // there. The onecli-gateway skill installed above auto-loads under the + // gateway and carries the same guidance. + {[]string{"codex"}, agentSpec{agentName: "Codex", baseDir: ".agents", skipHook: true, nativeProxyConfig: ".codex"}}, {[]string{"hermes"}, agentSpec{agentName: "Hermes", baseDir: ".hermes", skipHook: true, pluginGateway: true, dockerSandbox: true}}, {[]string{"opencode"}, agentSpec{agentName: "OpenCode", baseDir: ".opencode"}}, // OpenClaw loads skills from ~/.openclaw/skills; its hook system is its diff --git a/cmd/onecli/run_test.go b/cmd/onecli/run_test.go index 9fce213..5f66329 100644 --- a/cmd/onecli/run_test.go +++ b/cmd/onecli/run_test.go @@ -148,7 +148,7 @@ func TestAgentSkillDir(t *testing.T) { {"claude", agentSpec{agentName: "Claude Code", baseDir: ".claude"}, true}, {"cursor", agentSpec{agentName: "Cursor", baseDir: ".cursor", configDir: "Cursor"}, true}, {"agent", agentSpec{agentName: "Cursor", baseDir: ".cursor", configDir: "Cursor"}, true}, - {"codex", agentSpec{agentName: "Codex", baseDir: ".agents", nativeProxyConfig: ".codex", hooksFile: ".codex/hooks.json"}, true}, + {"codex", agentSpec{agentName: "Codex", baseDir: ".agents", skipHook: true, nativeProxyConfig: ".codex"}, true}, {"hermes", agentSpec{agentName: "Hermes", baseDir: ".hermes", skipHook: true, pluginGateway: true, dockerSandbox: true}, true}, {"opencode", agentSpec{agentName: "OpenCode", baseDir: ".opencode"}, true}, {"openclaw", agentSpec{agentName: "OpenClaw", baseDir: ".openclaw", skipHook: true, needsAnthropicKey: true}, true}, @@ -192,6 +192,27 @@ func TestEnsureEnv(t *testing.T) { }) } +func TestCodexSkipsGatewayHook(t *testing.T) { + // Codex renders injected hook context visibly in its transcript + // (Claude injects it silently), so `onecli run` must NOT register the + // gateway hook for Codex — the auto-loaded onecli-gateway skill carries + // the same guidance without the per-prompt noise. + spec, ok := agentSkillDir("codex") + if !ok { + t.Fatal("codex spec not found") + } + if !spec.skipHook { + t.Error("codex must set skipHook so the gateway hook is not registered") + } + if spec.hooksFile != "" { + t.Errorf("codex hooksFile = %q, want empty (hook is skipped)", spec.hooksFile) + } + // Claude, by contrast, keeps the hook (silent injection there). + if claude, _ := agentSkillDir("claude"); claude.skipHook { + t.Error("claude must keep the gateway hook (skipHook should be false)") + } +} + func TestProxyURLWithHost(t *testing.T) { tests := []struct{ name, raw, host, want string }{ {"rewrites host keeping port+creds", "http://aoc_tok:x@127.0.0.1:10255", "host.docker.internal", "http://aoc_tok:x@host.docker.internal:10255"}, @@ -773,7 +794,13 @@ func TestGatewayDetectHook_EmitsJSONEnvelope(t *testing.T) { }) } -func TestMaybeInstallGatewayHook_CodexHooksFile(t *testing.T) { +// TestMaybeInstallGatewayHook_DedicatedHooksFile covers the dedicated +// hooks-file registration path (a home-relative hooksFile, no matcher, plus +// the one-time trust notice), exercised via a Codex-style ~/.codex/hooks.json. +// Note: `onecli run` no longer registers this hook for Codex itself (Codex +// renders injected hook context visibly; see supportedAgents) — this verifies +// the underlying helper for any agent that opts into a dedicated hooks file. +func TestMaybeInstallGatewayHook_DedicatedHooksFile(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("test overrides HOME, which UserHomeDir ignores on windows") }