From 202a90dcb0ec837c718c430ad6ca4b5287e9bf8e Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:33:05 +0530 Subject: [PATCH 1/3] initialCommitCodex --- go.mod | 4 ++ go.sum | 2 - internal/commands/agenthooks/cx/hooks.go | 2 + internal/commands/agenthooks/cx/hooks_test.go | 1 + internal/commands/agenthooks/cx/install.go | 12 ++++++ .../commands/agenthooks/cx/install_test.go | 38 +++++++++++++++++++ internal/commands/hooks.go | 2 +- 7 files changed, 58 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 820d7faa..76a3fc87 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,10 @@ module github.com/checkmarx/ast-cli go 1.26.5 +// 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.5 github.com/Checkmarx/containers-resolver v1.0.34 diff --git a/go.sum b/go.sum index ecc531e6..476c1377 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.5 h1:4Og5JeBBg3SynAErAP76oGKrjoWrlduWRgg1V9IXjWo= -github.com/Checkmarx/ast-cx-hooks v1.0.5/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 90d4f48d..82fe2395 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 33f6caea..2341476a 100644 --- a/internal/commands/agenthooks/cx/install.go +++ b/internal/commands/agenthooks/cx/install.go @@ -103,6 +103,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 From 8627c7b4af084beebcd8b4108b87f36f367dc5f0 Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:30:13 +0530 Subject: [PATCH 2/3] codex-kics-check --- internal/commands/agenthooks/cx/hooks_test.go | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/internal/commands/agenthooks/cx/hooks_test.go b/internal/commands/agenthooks/cx/hooks_test.go index dd5fbde7..404a941c 100644 --- a/internal/commands/agenthooks/cx/hooks_test.go +++ b/internal/commands/agenthooks/cx/hooks_test.go @@ -794,3 +794,95 @@ func TestLogRemediationTelemetry_WithWrapper_Sends(t *testing.T) { logRemediationTelemetry("Claude", "SCA", "finding", "remediation") }) } + +// pipeStdio replaces os.Stdin/os.Stdout with temp files so agenthooks.Dispatch +// (which reads a real stdin JSON payload and writes a real stdout JSON verdict) +// can be driven end-to-end inside a unit test. Mirrors the helper of the same +// name in ast-cx-hooks's own codex_unified_test.go / copilot_unified_test.go. +func pipeStdio(t *testing.T, stdin string) func() string { + t.Helper() + + inFile, err := os.CreateTemp("", "codex-stdin-*.json") + assert.NoError(t, err) + t.Cleanup(func() { os.Remove(inFile.Name()) }) + _, err = inFile.WriteString(stdin) + assert.NoError(t, err) + _, err = inFile.Seek(0, 0) + assert.NoError(t, err) + + outFile, err := os.CreateTemp("", "codex-stdout-*.json") + assert.NoError(t, err) + t.Cleanup(func() { os.Remove(outFile.Name()) }) + + origIn, origOut := os.Stdin, os.Stdout + os.Stdin, os.Stdout = inFile, outFile + t.Cleanup(func() { os.Stdin, os.Stdout = origIn, origOut }) + + return func() string { + _ = outFile.Sync() + data, err := os.ReadFile(outFile.Name()) + assert.NoError(t, err) + return string(data) + } +} + +// TestCodexApplyPatch_KICSFinding_DeniesEndToEnd drives a genuine Codex CLI +// "apply_patch" PreToolUse payload — a V4A "Add File" patch introducing a +// Terraform file with a real KICS-detectable misconfiguration (an S3 bucket +// with public-read ACL) — through the actual codex-pre-file-write route: +// real stdin decoding, real Codex V4A patch parsing (codexDiff / +// codexPatchFilePath in ast-cx-hooks), the real cxBeforeFileEdit guardrail, +// and the real KICS extension gate + delta logic, down to a stubbed +// container-scan call. This proves the same KICS wiring that protects Claude +// (see TestCxBeforeFileEdit_KICSFinding_RejectsWithContext) is equally live +// for Codex, not merely present in cxBeforeFileEdit's agent-agnostic code. +func TestCodexApplyPatch_KICSFinding_DeniesEndToEnd(t *testing.T) { + resetHookGlobals(t) + + agenthooks.ClearRoutes() + RegisterGuardrails(&mock.JWTMockWrapper{}, &mock.FeatureFlagsMockWrapper{}, &mock.RealtimeScannerMockWrapper{}, mock.TelemetryMockWrapper{}) + t.Cleanup(agenthooks.ClearRoutes) + + // RegisterGuardrails wires up real SCA/KICS scanners; swap KICS's underlying + // scan for a stub (no Docker/Podman needed) and disable SCA so this test + // isolates the KICS guardrail's Codex wiring only. + kicsScanner = kics.NewScannerWithFunc(func(string, string) ([]iacrealtime.IacRealtimeResult, error) { + return []iacrealtime.IacRealtimeResult{{ + Title: "S3 Bucket Has Public Read Access", + SimilarityID: "sim-s3-public-read", + Severity: "HIGH", + Description: "S3 Bucket has an ACL defined which allows public READ Access", + Locations: []realtimeengine.Location{{Line: 2}}, + }}, nil + }) + scaScanner = nil + + patch := "*** Begin Patch\n" + + "*** Add File: main.tf\n" + + "+resource \"aws_s3_bucket\" \"bad\" {\n" + + "+ bucket = \"my-bad-bucket\"\n" + + "+ acl = \"public-read\"\n" + + "+}\n" + + "*** End Patch" + + reqBody := map[string]any{ + "session_id": "codex-kics-sess", + "cwd": t.TempDir(), + "tool_name": "apply_patch", + "tool_input": map[string]string{"command": patch}, + } + stdinBytes, err := json.Marshal(reqBody) + assert.NoError(t, err) + + readStdout := pipeStdio(t, string(stdinBytes)) + + origArgs := os.Args + os.Args = []string{"cx", "codex-pre-file-write"} + t.Cleanup(func() { os.Args = origArgs }) + + agenthooks.Dispatch() + + out := readStdout() + assert.Contains(t, out, `"permissionDecision":"deny"`) + assert.Contains(t, out, "KICS") +} From 67b6356148905faa22089cb86b70eecdbb72a1ff Mon Sep 17 00:00:00 2001 From: Hitesh Madgulkar <212497904+cx-hitesh-madgulkar@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:42:32 +0530 Subject: [PATCH 3/3] cx-hooks-1.0.9-added --- go.mod | 6 +----- go.sum | 2 ++ 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index c2630db6..52d1c16c 100644 --- a/go.mod +++ b/go.mod @@ -2,12 +2,8 @@ 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/ast-cx-hooks v1.0.9 github.com/Checkmarx/containers-resolver v1.0.34 github.com/Checkmarx/containers-types v1.0.9 github.com/Checkmarx/gen-ai-prompts v0.0.0-20240807143411-708ceec12b63 diff --git a/go.sum b/go.sum index d0346176..ba3d6114 100644 --- a/go.sum +++ b/go.sum @@ -65,6 +65,8 @@ 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.9 h1:NZ8Ekjxbe/0tj48Lv1rKVzwo2iGMXG06vD6HK0eZgio= +github.com/Checkmarx/ast-cx-hooks v1.0.9/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=