diff --git a/internal/aiagents/adapter/codex/adapter.go b/internal/aiagents/adapter/codex/adapter.go index e5efdfa..e19daa9 100644 --- a/internal/aiagents/adapter/codex/adapter.go +++ b/internal/aiagents/adapter/codex/adapter.go @@ -5,11 +5,11 @@ // // - ~/.codex/hooks.json — hook definitions (JSON) // - ~/.codex/config.toml — global config; install also sets -// `[features].codex_hooks = true` here so Codex actually invokes +// `[features].hooks = true` here so Codex actually invokes // hooks at runtime // // Uninstall removes DMG-owned hook entries from hooks.json but does -// NOT revert the codex_hooks feature flag — the user may have wired +// NOT revert the hooks feature flag — the user may have wired // up other tools' hooks that depend on it. // // Restore + Status are intentionally absent (see adapter.Adapter for @@ -91,7 +91,7 @@ func (a *Adapter) Detect(ctx context.Context, exec executor.Executor) (adapter.D } // Install adds DMG-owned hooks to hooks.json and ensures the -// `[features].codex_hooks=true` flag in config.toml. +// `[features].hooks=true` flag in config.toml. // // Multi-file safety: every output buffer (hooks.json + config.toml) // is loaded, validated, and encoded BEFORE the first write happens — @@ -150,13 +150,13 @@ func (a *Adapter) Install(ctx context.Context) (adapter.InstallResult, error) { } res.CreatedDirs = appendUnique(res.CreatedDirs, cfgWR.CreatedDirs...) } - res.Notes = append(res.Notes, "enabled [features].codex_hooks=true in "+a.configPath) + res.Notes = append(res.Notes, "enabled [features].hooks=true in "+a.configPath) } return res, nil } // Uninstall removes DMG-owned hook entries from hooks.json. The -// `[features].codex_hooks` flag in config.toml is intentionally NOT +// `[features].hooks` flag in config.toml is intentionally NOT // reverted — the user may have other tools' hooks that depend on it // being enabled. // diff --git a/internal/aiagents/adapter/codex/adapter_test.go b/internal/aiagents/adapter/codex/adapter_test.go index c54c331..fa23a21 100644 --- a/internal/aiagents/adapter/codex/adapter_test.go +++ b/internal/aiagents/adapter/codex/adapter_test.go @@ -477,8 +477,8 @@ func TestInstallCreatesHooksAndFeatureFlag(t *testing.T) { if !ok { t.Fatalf("features table missing: %v", cfgMap) } - if features["codex_hooks"] != true { - t.Errorf("codex_hooks not true: %v", features) + if features["hooks"] != true { + t.Errorf("hooks not true: %v", features) } // InstallResult tracks both files written under root chown. @@ -539,8 +539,8 @@ other_flag = true if features["other_flag"] != true { t.Errorf("unrelated features key lost: %v", features) } - if features["codex_hooks"] != true { - t.Errorf("codex_hooks not enabled: %v", features) + if features["hooks"] != true { + t.Errorf("hooks not enabled: %v", features) } } @@ -732,16 +732,25 @@ enabled = true `sandbox = "workspace-write"`, "[telemetry]", "enabled = true", - "codex_hooks = true", } { if !strings.Contains(s, want) { t.Errorf("expected %q in output; got: %s", want, s) } } - // Order: telemetry must still come AFTER features (which now contains codex_hooks). + // New `hooks = true` key must be present under [features], asserted structurally. + cfgMap := readTOML(t, cfg) + features, ok := cfgMap["features"].(map[string]any) + if !ok { + t.Fatalf("features table missing: %v", cfgMap) + } + if features["hooks"] != true { + t.Errorf("hooks not true under [features]: %v", features) + } + // Order: telemetry must still come AFTER features (which now contains hooks). + // Anchor to start-of-line so the substring can't match a `codex_hooks` legacy line. featIdx := strings.Index(s, "[features]") telIdx := strings.Index(s, "[telemetry]") - chIdx := strings.Index(s, "codex_hooks") + chIdx := strings.Index(s, "\nhooks = true") if !(featIdx < chIdx && chIdx < telIdx) { t.Errorf("table order disturbed: %s", s) } @@ -777,7 +786,7 @@ func TestInstallSecondInstallIsByteStableNoOp(t *testing.T) { func TestInstallNoOpDoesNotRewriteConfigTOML(t *testing.T) { a, _, _, cfg := withCodexFiles(t, "", `[features] -codex_hooks = true +hooks = true sandbox = "workspace-write" `) original, _ := os.ReadFile(cfg) @@ -928,7 +937,7 @@ func TestUninstallPreservesHooksJSONUserKeyOrder(t *testing.T) { } func TestUninstallLeavesFeatureFlagEnabled(t *testing.T) { - // Uninstall must NOT revert `[features].codex_hooks = true`. Other + // Uninstall must NOT revert `[features].hooks = true`. Other // tools may have wired up their own hooks that depend on it being on. a, _, _, cfg := newCodexHome(t) if _, err := a.Install(context.Background()); err != nil { @@ -942,7 +951,7 @@ func TestUninstallLeavesFeatureFlagEnabled(t *testing.T) { if !ok { t.Fatalf("features table missing after uninstall: %v", cfgMap) } - if features["codex_hooks"] != true { - t.Errorf("codex_hooks was reverted on uninstall: %v", features) + if features["hooks"] != true { + t.Errorf("hooks was reverted on uninstall: %v", features) } } diff --git a/internal/aiagents/cli/uninstall_test.go b/internal/aiagents/cli/uninstall_test.go index 9eec4e1..8451c4c 100644 --- a/internal/aiagents/cli/uninstall_test.go +++ b/internal/aiagents/cli/uninstall_test.go @@ -6,6 +6,7 @@ import ( "errors" "os" "path/filepath" + "regexp" "strings" "testing" @@ -263,7 +264,7 @@ func TestRunUninstall_ExplicitAgentSkipsDetection(t *testing.T) { // TestRunUninstall_CodexLeavesFeatureFlag pins the invariant that // uninstall removes hook entries from hooks.json but does NOT revert -// [features].codex_hooks=true in config.toml. Other tools' hooks may +// [features].hooks=true in config.toml. Other tools' hooks may // depend on that flag staying enabled. func TestRunUninstall_CodexLeavesFeatureFlag(t *testing.T) { home, m := runInstallForTest(t, "codex") @@ -276,8 +277,8 @@ func TestRunUninstall_CodexLeavesFeatureFlag(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.Contains(string(beforeCfg), "codex_hooks") { - t.Fatalf("seed broken: install didn't set codex_hooks flag: %s", string(beforeCfg)) + if !regexp.MustCompile(`(?m)^hooks[ \t]*=[ \t]*true`).Match(beforeCfg) { + t.Fatalf("seed broken: install didn't set hooks flag: %s", string(beforeCfg)) } var stdout, stderr bytes.Buffer diff --git a/internal/aiagents/configedit/json.go b/internal/aiagents/configedit/json.go index 62dea04..d949363 100644 --- a/internal/aiagents/configedit/json.go +++ b/internal/aiagents/configedit/json.go @@ -3,7 +3,7 @@ // rewrite key order, drop comments (TOML), and renormalize whitespace. // configedit performs path-targeted edits backed by tidwall/gjson and // tidwall/sjson for JSON, plus a narrow regex+mask patcher for the -// codex_hooks TOML feature flag. +// Codex hooks TOML feature flag. // // Scope is intentionally narrow: only what the claudecode and codex // adapters need. diff --git a/internal/aiagents/configedit/toml.go b/internal/aiagents/configedit/toml.go index 142fe36..9e8f7c3 100644 --- a/internal/aiagents/configedit/toml.go +++ b/internal/aiagents/configedit/toml.go @@ -8,22 +8,26 @@ import ( toml "github.com/pelletier/go-toml/v2" ) -// EnsureCodexHooksFlag returns the input bytes with `[features].codex_hooks +// EnsureCodexHooksFlag returns the input bytes with `[features].hooks // = true` ensured. All bytes outside the touched line/section are // preserved exactly. The boolean is true when the input changed. // // Behavior: -// - If `codex_hooks = true` already exists under [features], no change. -// - If `codex_hooks = false` exists under [features], only the value +// - If `hooks = true` already exists under [features], no change. +// - If `hooks = false` exists under [features], only the value // token is rewritten to `true`. -// - If [features] exists without the key, `codex_hooks = true` is +// - If [features] exists without the key, `hooks = true` is // inserted on its own line immediately after the table header. // - If [features] does not exist, a new `[features]` table is appended -// at the end of the file with `codex_hooks = true`. +// at the end of the file with `hooks = true`. +// +// A pre-existing deprecated `codex_hooks` line is left untouched; the +// user can clean it up. Codex CLI accepts both keys today but warns on +// the old one — see https://developers.openai.com/codex/config-basic#feature-flags. // // Multi-line strings (`"""..."""`, `”'...”'`) and comments are masked // before pattern matching so that user content cannot trick the -// scanner into treating the literal text `[features]` or `codex_hooks = +// scanner into treating the literal text `[features]` or `hooks = // true` inside a string as a real table header or key. // // The patched output is validated by go-toml/v2 before return; if the @@ -47,7 +51,7 @@ func EnsureCodexHooksFlag(data []byte) ([]byte, bool, error) { if len(data) > 0 { b.WriteByte('\n') } - b.WriteString("[features]\ncodex_hooks = true\n") + b.WriteString("[features]\nhooks = true\n") out, changed = b.Bytes(), true } else if loc := codexHooksLineRE.FindSubmatchIndex(masked[start:end]); loc != nil { valStart := start + loc[4] @@ -61,10 +65,10 @@ func EnsureCodexHooksFlag(data []byte) ([]byte, bool, error) { b.Write(data[valEnd:]) out, changed = b.Bytes(), true } else { - // Insert codex_hooks = true immediately after the [features] header line. + // Insert hooks = true immediately after the [features] header line. var b bytes.Buffer b.Write(data[:headerEnd]) - b.WriteString("codex_hooks = true\n") + b.WriteString("hooks = true\n") b.Write(data[headerEnd:]) out, changed = b.Bytes(), true } @@ -79,7 +83,7 @@ func EnsureCodexHooksFlag(data []byte) ([]byte, bool, error) { } // CodexHooksEnabled reports whether the bytes contain -// `[features].codex_hooks = true`. Multi-line strings and comments are +// `[features].hooks = true`. Multi-line strings and comments are // masked so a literal containing the same text in a docstring is not // misread as the real flag. func CodexHooksEnabled(data []byte) bool { @@ -98,7 +102,7 @@ func CodexHooksEnabled(data []byte) bool { var ( featuresHeaderRE = regexp.MustCompile(`(?m)^[ \t]*\[[ \t]*features[ \t]*\][ \t]*(#.*)?$`) anyHeaderRE = regexp.MustCompile(`(?m)^[ \t]*\[\[?[^\]\n]+\]\]?[ \t]*(#.*)?$`) - codexHooksLineRE = regexp.MustCompile(`(?m)^([ \t]*codex_hooks[ \t]*=[ \t]*)(true|false)([ \t]*(?:#.*)?)$`) + codexHooksLineRE = regexp.MustCompile(`(?m)^([ \t]*hooks[ \t]*=[ \t]*)(true|false)([ \t]*(?:#.*)?)$`) ) // findFeaturesSection scans masked TOML bytes and returns: diff --git a/internal/aiagents/configedit/toml_test.go b/internal/aiagents/configedit/toml_test.go index 1caaa9e..929c3ff 100644 --- a/internal/aiagents/configedit/toml_test.go +++ b/internal/aiagents/configedit/toml_test.go @@ -21,8 +21,8 @@ func TestEnsureCodexHooksFlagAppendsWhenAbsent(t *testing.T) { if !strings.Contains(s, "[features]") { t.Errorf("missing [features]: %s", s) } - if !strings.Contains(s, "codex_hooks = true") { - t.Errorf("missing codex_hooks: %s", s) + if !strings.Contains(s, "\nhooks = true") { + t.Errorf("missing hooks: %s", s) } // Original line preserved. if !strings.HasPrefix(s, `model = "gpt-5"`) { @@ -48,8 +48,8 @@ other_flag = true t.Errorf("expected changed=true") } s := string(out) - if !strings.Contains(s, "codex_hooks = true") { - t.Errorf("missing codex_hooks: %s", s) + if !strings.Contains(s, "\nhooks = true") { + t.Errorf("missing hooks: %s", s) } // Original keys still present and order preserved. if !strings.Contains(s, "other_flag = true") { @@ -62,7 +62,7 @@ other_flag = true func TestEnsureCodexHooksFlagFlipsFalseToTrue(t *testing.T) { in := []byte(`[features] -codex_hooks = false +hooks = false `) out, changed, err := EnsureCodexHooksFlag(in) if err != nil { @@ -71,17 +71,17 @@ codex_hooks = false if !changed { t.Errorf("expected changed=true") } - if !strings.Contains(string(out), "codex_hooks = true") { + if !strings.Contains(string(out), "\nhooks = true") { t.Errorf("flag not flipped: %s", out) } - if strings.Contains(string(out), "codex_hooks = false") { + if strings.Contains(string(out), "\nhooks = false") { t.Errorf("old false value still present: %s", out) } } func TestEnsureCodexHooksFlagNoOpWhenTrue(t *testing.T) { in := []byte(`[features] -codex_hooks = true +hooks = true other = false `) out, changed, err := EnsureCodexHooksFlag(in) @@ -122,26 +122,27 @@ enabled = true `sandbox = "workspace-write"`, "[telemetry]", "enabled = true", - "codex_hooks = true", + "\nhooks = true", } { if !strings.Contains(s, want) { t.Errorf("expected output to contain %q; got %s", want, s) } } - // codex_hooks must land in [features], not [telemetry]. + // hooks must land in [features], not [telemetry]. + // Anchor to line-start so a future `codex_hooks` line can't satisfy the index. featStart := strings.Index(s, "[features]") telStart := strings.Index(s, "[telemetry]") - codexAt := strings.Index(s, "codex_hooks") + codexAt := strings.Index(s, "\nhooks = true") if !(featStart < codexAt && codexAt < telStart) { - t.Errorf("codex_hooks landed outside [features]: %s", s) + t.Errorf("hooks landed outside [features]: %s", s) } } func TestEnsureCodexHooksFlagIgnoresLiteralsInsideMultilineStrings(t *testing.T) { - // The literal text `[features]` and `codex_hooks = true` appear + // The literal text `[features]` and `hooks = true` appear // inside a triple-quoted string. The patcher must NOT treat them as // real TOML structure, and must still append a real [features] table. - in := []byte("docstring = \"\"\"\n[features]\ncodex_hooks = true\n\"\"\"\n") + in := []byte("docstring = \"\"\"\n[features]\nhooks = true\n\"\"\"\n") out, changed, err := EnsureCodexHooksFlag(in) if err != nil { t.Fatal(err) @@ -152,10 +153,10 @@ func TestEnsureCodexHooksFlagIgnoresLiteralsInsideMultilineStrings(t *testing.T) // Output must still contain the docstring intact and a NEW real // [features] table at the end. s := string(out) - if !strings.Contains(s, "docstring = \"\"\"\n[features]\ncodex_hooks = true\n\"\"\"") { + if !strings.Contains(s, "docstring = \"\"\"\n[features]\nhooks = true\n\"\"\"") { t.Errorf("docstring corrupted: %s", s) } - if !strings.HasSuffix(s, "[features]\ncodex_hooks = true\n") { + if !strings.HasSuffix(s, "[features]\nhooks = true\n") { t.Errorf("real [features] table not appended: %s", s) } // Validates as TOML. @@ -168,7 +169,7 @@ func TestEnsureCodexHooksFlagIgnoresLiteralsInsideMultilineStrings(t *testing.T) func TestCodexHooksEnabledIgnoresLiteralsInsideStrings(t *testing.T) { // The flag appears inside a literal multiline string, NOT as a real // key. CodexHooksEnabled must report false. - in := []byte("docstring = \"\"\"\n[features]\ncodex_hooks = true\n\"\"\"\n") + in := []byte("docstring = \"\"\"\n[features]\nhooks = true\n\"\"\"\n") if CodexHooksEnabled(in) { t.Errorf("multiline string content must not be detected as enabled flag") } @@ -187,7 +188,7 @@ func TestEnsureCodexHooksFlagRejectsPatchProducingInvalidTOML(t *testing.T) { } func TestCodexHooksEnabledIgnoresCommentedFlag(t *testing.T) { - in := []byte("# [features]\n# codex_hooks = true\n") + in := []byte("# [features]\n# hooks = true\n") if CodexHooksEnabled(in) { t.Fatal("commented flag must not count as enabled") } @@ -201,9 +202,9 @@ func TestCodexHooksEnabled(t *testing.T) { }{ {"absent", `model = "gpt-5"`, false}, {"missing key", "[features]\nother = true\n", false}, - {"false", "[features]\ncodex_hooks = false\n", false}, - {"true", "[features]\ncodex_hooks = true\n", true}, - {"true with comment", "[features]\ncodex_hooks = true # on\n", true}, + {"false", "[features]\nhooks = false\n", false}, + {"true", "[features]\nhooks = true\n", true}, + {"true with comment", "[features]\nhooks = true # on\n", true}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) {