diff --git a/go.mod b/go.mod index 27a6d5f8..c2630db6 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module github.com/checkmarx/ast-cli go 1.26.6 +// TODO: remove once ast-cx-hooks publishes a release with Codex CLI support +// (currently only on the "codex" branch, commit 8bafa41). +replace github.com/Checkmarx/ast-cx-hooks => ../ast-cx-hooks + require ( github.com/Checkmarx/ast-cx-hooks v1.0.6 github.com/Checkmarx/containers-resolver v1.0.34 diff --git a/go.sum b/go.sum index 34b4cffe..d0346176 100644 --- a/go.sum +++ b/go.sum @@ -65,8 +65,6 @@ github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Checkmarx/ast-cx-hooks v1.0.6 h1:8/Kcl9V0XKeY1vgTKJR6eIfXXoa4c9DgUOBuY1Ms268= -github.com/Checkmarx/ast-cx-hooks v1.0.6/go.mod h1:GPHk8IJHQlCW7l8ye9/Bij57zYQGRG+pxJPiGgsR8cY= github.com/Checkmarx/containers-images-extractor v1.0.22 h1:kJZgwk28LwJZ7Xky+kzwL+JSZOlpwrGsZQhhz4L2t6s= github.com/Checkmarx/containers-images-extractor v1.0.22/go.mod h1:HyzVb8TtTDf56hGlSakalPXtzjJ6VhTYe9fmAcOS+V8= github.com/Checkmarx/containers-resolver v1.0.34 h1:KULN8s8xb1tQtdH4yzHVdwN8GyLqtPCAkFWra10k7V0= diff --git a/internal/commands/agenthooks/cx/hooks.go b/internal/commands/agenthooks/cx/hooks.go index 0bacd8b3..c333685a 100644 --- a/internal/commands/agenthooks/cx/hooks.go +++ b/internal/commands/agenthooks/cx/hooks.go @@ -276,6 +276,8 @@ func agentToString(agent agenthooks.AgentID) string { return "Droid" case agenthooks.AgentWindsurf: return "Windsurf" + case agenthooks.AgentCodex: + return "Codex" default: return "Unknown" } diff --git a/internal/commands/agenthooks/cx/hooks_test.go b/internal/commands/agenthooks/cx/hooks_test.go index e11f2da7..dd5fbde7 100644 --- a/internal/commands/agenthooks/cx/hooks_test.go +++ b/internal/commands/agenthooks/cx/hooks_test.go @@ -621,6 +621,7 @@ func TestAgentToString(t *testing.T) { {"gemini", agenthooks.AgentGemini, "Gemini"}, {"droid", agenthooks.AgentDroid, "Droid"}, {"windsurf", agenthooks.AgentWindsurf, "Windsurf"}, + {"codex", agenthooks.AgentCodex, "Codex"}, {"unknown", agenthooks.AgentID("something-else"), "Unknown"}, } for _, tt := range tests { diff --git a/internal/commands/agenthooks/cx/install.go b/internal/commands/agenthooks/cx/install.go index a47e829a..9b3a7504 100644 --- a/internal/commands/agenthooks/cx/install.go +++ b/internal/commands/agenthooks/cx/install.go @@ -104,6 +104,18 @@ var Agents = []Agent{ {"copilot-cli-user-prompt-submit", "Gate GitHub Copilot CLI prompt"}, }, }, + { + ID: "codex", + DisplayName: "OpenAI Codex CLI", + ConfigPath: "~/.codex/hooks.json", + Install: install.InstallCodex, + Routes: []Route{ + {"codex-stop", "Codex CLI agent finished"}, + {"codex-pre-tool-use", "Gate Codex CLI tool use"}, + {"codex-pre-file-write", "Gate Codex CLI file write"}, + {"codex-user-prompt-submit", "Gate Codex CLI prompt"}, + }, + }, } // FindAgent returns the Agent with the given ID, or nil if not found. diff --git a/internal/commands/agenthooks/cx/install_test.go b/internal/commands/agenthooks/cx/install_test.go index 1692bd60..e9d162b6 100644 --- a/internal/commands/agenthooks/cx/install_test.go +++ b/internal/commands/agenthooks/cx/install_test.go @@ -42,6 +42,44 @@ func TestFindAgentCopilot(t *testing.T) { } } +// TestFindAgentCodex pins the OpenAI Codex CLI agent entry: its config path +// and the curated route set the installer mirrors. The route Use names must match +// the codex-* routes ast-cx-hooks registers, or `cx hooks agenthooks install +// codex` would write commands that don't resolve. +func TestFindAgentCodex(t *testing.T) { + agent := FindAgent("codex") + if agent == nil { + t.Fatal("FindAgent(\"codex\") returned nil; Codex agent not registered") + } + if agent.DisplayName != "OpenAI Codex CLI" { + t.Errorf("DisplayName = %q, want %q", agent.DisplayName, "OpenAI Codex CLI") + } + if agent.ConfigPath != "~/.codex/hooks.json" { + t.Errorf("ConfigPath = %q, want %q", agent.ConfigPath, "~/.codex/hooks.json") + } + if agent.Install == nil { + t.Error("Install func is nil") + } + + wantRoutes := []string{ + "codex-stop", + "codex-pre-tool-use", + "codex-pre-file-write", + "codex-user-prompt-submit", + } + if len(agent.Routes) != len(wantRoutes) { + t.Fatalf("got %d routes, want %d: %+v", len(agent.Routes), len(wantRoutes), agent.Routes) + } + for i, want := range wantRoutes { + if agent.Routes[i].Use != want { + t.Errorf("Routes[%d].Use = %q, want %q", i, agent.Routes[i].Use, want) + } + if agent.Routes[i].Short == "" { + t.Errorf("Routes[%d] (%q) has empty Short description", i, want) + } + } +} + // TestFindAgentUnknown verifies FindAgent returns nil for an unregistered id. func TestFindAgentUnknown(t *testing.T) { if a := FindAgent("not-a-real-agent"); a != nil { diff --git a/internal/commands/hooks.go b/internal/commands/hooks.go index 2f33419a..65c9b5c4 100644 --- a/internal/commands/hooks.go +++ b/internal/commands/hooks.go @@ -14,7 +14,7 @@ func NewHooksCommand(jwtWrapper wrappers.JWTWrapper, featureFlagsWrapper wrapper hooksCmd := &cobra.Command{ Use: "hooks", Short: "Manage Git hooks and AI coding agent hooks", - Long: "The hooks command manages Git hooks for secret detection and AI coding agent hooks for Claude, Cursor, Windsurf, Factory Droid, Gemini, and GitHub Copilot CLI.", + Long: "The hooks command manages Git hooks for secret detection and AI coding agent hooks for Claude, Cursor, Windsurf, Factory Droid, Gemini, GitHub Copilot CLI, and OpenAI Codex CLI.", Example: heredoc.Doc( ` $ cx hooks pre-commit secrets-install-git-hook