From bf89a542dabb8cef0102cdb0f69bb3600b2f8e34 Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Wed, 15 Jul 2026 10:56:01 +0200 Subject: [PATCH 1/4] fix(codex): stop writing .codex/config.toml, hooks are on by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex enables hooks by default since codex-cli 0.124.0 (openai/codex#19012); the feature flag only exists as an opt-out now. Writing the flag to a project-local .codex/config.toml was not just unnecessary — inside Codex's reserved /agents tree the TOML file gets picked up by the agent-role scanner and rejected at every startup as a malformed agent role definition (#842). Drop the flag write entirely and self-heal: a stale .codex/config.toml containing only the feature-flag content older entire versions wrote is removed on the next enable. Detection is line-anchored so a file carrying any user content is left byte-identical. Fixes #842 Co-Authored-By: Claude Fable 5 Entire-Checkpoint: 01KXJFTYXV9F4EN483F10H8H77 --- cmd/entire/cli/agent/codex/hooks.go | 126 +++++++++++------------ cmd/entire/cli/agent/codex/hooks_test.go | 111 +++++++++++++++----- 2 files changed, 147 insertions(+), 90 deletions(-) diff --git a/cmd/entire/cli/agent/codex/hooks.go b/cmd/entire/cli/agent/codex/hooks.go index e3c0d01642..8cf72094c5 100644 --- a/cmd/entire/cli/agent/codex/hooks.go +++ b/cmd/entire/cli/agent/codex/hooks.go @@ -118,10 +118,10 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool } if count == 0 { - // Still ensure the feature flag is configured even if hooks - // were already present (e.g., manually installed). - if err := ensureProjectFeatureEnabled(repoRoot); err != nil { - return 0, fmt.Errorf("failed to enable codex_hooks feature: %w", err) + // Still self-heal a stale feature-flag config.toml left by an + // older entire version, even if hooks were already present. + if err := cleanupStaleFeatureConfig(repoRoot); err != nil { + return 0, err } return 0, nil } @@ -158,10 +158,11 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool return 0, fmt.Errorf("failed to write hooks.json: %w", err) } - // Enable the codex_hooks feature in the project-level .codex/config.toml. - // This keeps the feature flag per-repo rather than global. - if err := ensureProjectFeatureEnabled(repoRoot); err != nil { - return count, fmt.Errorf("failed to enable codex_hooks feature: %w", err) + // Hooks are enabled by default in Codex (since 0.124.0), so no feature + // flag is written. Self-heal any stale .codex/config.toml an older + // entire version left behind. + if err := cleanupStaleFeatureConfig(repoRoot); err != nil { + return count, err } return count, nil @@ -362,79 +363,74 @@ func removeEntireHooks(groups []MatcherGroup) []MatcherGroup { // configFileName is the Codex config file name. const configFileName = "config.toml" -// featureLine is the TOML line that enables the hooks feature. The flag was -// renamed from `codex_hooks` to `hooks` in Codex 0.129.0; the old name is -// still accepted as a legacy alias but emits a deprecation warning at -// every startup. ensureProjectFeatureEnabled rewrites the legacy form when -// it sees it. +// featureLine / legacyFeatureLine are the TOML feature-flag lines older +// entire versions wrote to a project-local .codex/config.toml back when +// Codex hooks were experimental (the flag was renamed from `codex_hooks` +// to `hooks` in Codex 0.129.0). Hooks are enabled by default since Codex +// 0.124.0 (openai/codex#19012), so the flag is no longer written — these +// constants only identify stale files for cleanupStaleFeatureConfig. const ( featureLine = "hooks = true" legacyFeatureLine = "codex_hooks = true" ) -// ensureProjectFeatureEnabled writes features.hooks = true to the -// project-level .codex/config.toml. This keeps the feature flag per-repo. -// Replaces the deprecated codex_hooks = true line if it's present. -func ensureProjectFeatureEnabled(repoRoot string) error { +// cleanupStaleFeatureConfig removes a project-local .codex/config.toml left +// behind by an older entire version that wrote the hooks feature flag there. +// The flag is obsolete (hooks are on by default since Codex 0.124.0), and a +// leftover config.toml is actively harmful when the repo lives inside +// /agents — Codex recursively scans that tree for agent-role +// TOML files and rejects the leftover at every startup as a "malformed +// agent role definition" (entireio/cli#842). Only removes the file when +// every non-blank line is one of the exact feature-flag lines or the +// [features] header this package used to write (see +// isEntireManagedLocalConfig) — a file carrying any unrelated user content +// is left alone. +func cleanupStaleFeatureConfig(repoRoot string) error { configPath := filepath.Join(repoRoot, ".codex", configFileName) data, err := os.ReadFile(configPath) //nolint:gosec // path constructed from repo root - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read config.toml: %w", err) - } - - content := string(data) - hasNew := containsFeatureLine(content, featureLine) - hasLegacy := containsFeatureLine(content, legacyFeatureLine) - switch { - case hasNew && hasLegacy: - content = stripLegacyFeatureLine(content) - case hasNew: - return nil - case hasLegacy: - content = strings.Replace(content, legacyFeatureLine, featureLine, 1) - case strings.Contains(content, "[features]"): - content = strings.Replace(content, "[features]", "[features]\n"+featureLine, 1) - default: - if len(content) > 0 && !strings.HasSuffix(content, "\n") { - content += "\n" + if err != nil { + if os.IsNotExist(err) { + return nil } - content += "\n[features]\n" + featureLine + "\n" + return fmt.Errorf("failed to read stale config.toml: %w", err) } - if err := os.MkdirAll(filepath.Dir(configPath), 0o750); err != nil { - return fmt.Errorf("failed to create .codex directory: %w", err) + if !isEntireManagedLocalConfig(string(data)) { + return nil } - if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { //nolint:gosec // path constructed from repo root - return fmt.Errorf("failed to write config.toml: %w", err) + if err := os.Remove(configPath); err != nil { + return fmt.Errorf("failed to remove stale config.toml: %w", err) } return nil } -// containsFeatureLine checks for an exact line match. A plain -// strings.Contains is wrong because "hooks = true" is a substring of -// "codex_hooks = true" — without the line-boundary anchor we'd treat the -// legacy form as if the new form was already present. -func containsFeatureLine(content, line string) bool { - for _, raw := range strings.Split(content, "\n") { - if strings.TrimSpace(raw) == line { - return true +// isEntireManagedLocalConfig reports whether content consists of nothing but +// the [features] header and feature-flag lines this package used to write +// (plus blank lines), with at least one such managed line present. Any other +// non-blank line — a user's own setting or a comment — makes the file +// unmanaged, so cleanup leaves it untouched. +// +// The check is line-anchored on purpose: a substring scan could mistake a +// user's `webhooks = true` for our `hooks = true`, or match `[features]` +// inside an unrelated value. A whole-line match cannot mistake a user's +// line for ours. +func isEntireManagedLocalConfig(content string) bool { + managed := map[string]bool{ + "[features]": true, + featureLine: true, + legacyFeatureLine: true, + } + sawManaged := false + for raw := range strings.SplitSeq(content, "\n") { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + continue } + if !managed[trimmed] { + return false + } + sawManaged = true } - return false -} - -// stripLegacyFeatureLine removes the deprecated `codex_hooks = true` line -// from a TOML config string, dropping a trailing blank line so the file -// stays tidy. The new `hooks = true` is added separately by the caller. -func stripLegacyFeatureLine(content string) string { - idx := strings.Index(content, legacyFeatureLine) - if idx < 0 { - return content - } - end := idx + len(legacyFeatureLine) - if end < len(content) && content[end] == '\n' { - end++ - } - return content[:idx] + content[end:] + return sawManaged } diff --git a/cmd/entire/cli/agent/codex/hooks_test.go b/cmd/entire/cli/agent/codex/hooks_test.go index c9f5eb99b1..d9e9015423 100644 --- a/cmd/entire/cli/agent/codex/hooks_test.go +++ b/cmd/entire/cli/agent/codex/hooks_test.go @@ -42,14 +42,13 @@ func TestInstallHooks_CreatesConfig(t *testing.T) { assertHookCommand(t, hooksFile.Hooks.Stop, agentpkg.WrapProductionSilentHookCommand("entire hooks codex stop"), "Stop") assertHookCommand(t, hooksFile.Hooks.PostToolUse, agentpkg.WrapProductionSilentHookCommand("entire hooks codex post-tool-use"), "PostToolUse") - // Verify project-level config.toml enables the hooks feature (per-repo) + // Hooks are enabled by default in Codex, so no .codex/config.toml is + // written. A TOML file there is actively harmful when the repo lives + // inside /agents, where Codex's agent-role scanner rejects + // it at startup (entireio/cli#842). projectConfig := filepath.Join(tempDir, ".codex", configFileName) - projectData, err := os.ReadFile(projectConfig) - require.NoError(t, err) - require.Contains(t, string(projectData), "hooks = true") - require.NotContains(t, string(projectData), "codex_hooks = true", - "deprecated codex_hooks line must not be written by fresh installs") - require.Contains(t, string(projectData), "[features]") + _, err = os.Stat(projectConfig) + require.True(t, os.IsNotExist(err), "install must not create .codex/config.toml") } func TestInstallHooks_WindowsWrapperProbeSuccessKeepsWrappedCommands(t *testing.T) { @@ -362,30 +361,92 @@ func TestInstallHooks_DoesNotModifyUserConfig(t *testing.T) { require.NotContains(t, string(configData), `trust_level = "trusted"`) } -// TestInstallHooks_RewritesLegacyFeatureLine pins the rule that an existing -// `codex_hooks = true` line — written by older entire CLI versions — must -// be rewritten to the new `hooks = true` form on the next install. Codex -// 0.129.0 still accepts the legacy alias but prints a deprecation warning -// at every startup; rewriting silences it without forcing the user to -// touch their .codex/config.toml. -func TestInstallHooks_RewritesLegacyFeatureLine(t *testing.T) { - tempDir := setupTestEnv(t) +// TestInstallHooks_CleansUpStaleFeatureConfig pins the self-heal: a +// project-local .codex/config.toml containing only the feature-flag content +// older entire versions wrote (either the current `hooks = true` form or the +// legacy `codex_hooks = true` form) is removed on the next install. Hooks +// are enabled by default in Codex, and a leftover TOML file under +// /agents triggers a "malformed agent role definition" warning +// at every Codex startup (entireio/cli#842). +func TestInstallHooks_CleansUpStaleFeatureConfig(t *testing.T) { + staleContents := map[string]string{ + "current form": "[features]\nhooks = true\n", + "legacy form": "[features]\ncodex_hooks = true\n", + "both forms": "[features]\ncodex_hooks = true\nhooks = true\n", + "extra blanks": "\n[features]\n\nhooks = true\n\n", + } + for name, content := range staleContents { + t.Run(name, func(t *testing.T) { + tempDir := setupTestEnv(t) + + codexDir := filepath.Join(tempDir, ".codex") + require.NoError(t, os.MkdirAll(codexDir, 0o750)) + configPath := filepath.Join(codexDir, configFileName) + require.NoError(t, os.WriteFile(configPath, []byte(content), 0o600)) + + ag := &CodexAgent{} + _, err := ag.InstallHooks(context.Background(), false, false) + require.NoError(t, err) + + _, err = os.Stat(configPath) + require.True(t, os.IsNotExist(err), "stale entire-managed config.toml must be removed") + }) + } +} - codexDir := filepath.Join(tempDir, ".codex") - require.NoError(t, os.MkdirAll(codexDir, 0o750)) - existingConfig := "[features]\ncodex_hooks = true\n" - configPath := filepath.Join(codexDir, configFileName) - require.NoError(t, os.WriteFile(configPath, []byte(existingConfig), 0o600)) +// TestInstallHooks_CleansUpStaleFeatureConfig_WhenHooksAlreadyInstalled pins +// that the self-heal also runs on a re-enable where hooks.json is already +// up to date (the count == 0 path), so upgrading entire fixes the leftover +// without requiring a force reinstall. +func TestInstallHooks_CleansUpStaleFeatureConfig_WhenHooksAlreadyInstalled(t *testing.T) { + tempDir := setupTestEnv(t) ag := &CodexAgent{} - _, err := ag.InstallHooks(context.Background(), false, false) + count, err := ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) + require.Equal(t, 4, count) + + configPath := filepath.Join(tempDir, ".codex", configFileName) + require.NoError(t, os.WriteFile(configPath, []byte("[features]\nhooks = true\n"), 0o600)) - configData, err := os.ReadFile(configPath) + count, err = ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) - require.Contains(t, string(configData), "hooks = true") - require.NotContains(t, string(configData), "codex_hooks = true", - "legacy codex_hooks line must be replaced, not left alongside the new form") + require.Equal(t, 0, count) + + _, err = os.Stat(configPath) + require.True(t, os.IsNotExist(err), "stale config.toml must be removed even when hooks.json is already current") +} + +// TestInstallHooks_PreservesUserLocalConfig pins that cleanup never deletes a +// .codex/config.toml carrying anything beyond the exact feature-flag content +// this package used to write — a user's own settings, comments, or lines that +// merely contain our tokens as substrings (`webhooks = true`) make the file +// unmanaged and it is left byte-identical. +func TestInstallHooks_PreservesUserLocalConfig(t *testing.T) { + userContents := map[string]string{ + "user setting alongside flag": "[features]\nhooks = true\nsandbox = \"workspace-write\"\n", + "comment": "# my codex config\n[features]\nhooks = true\n", + "substring lookalike": "[features]\nwebhooks = true\n", + "unrelated file": "model = \"gpt-4.1\"\n", + } + for name, content := range userContents { + t.Run(name, func(t *testing.T) { + tempDir := setupTestEnv(t) + + codexDir := filepath.Join(tempDir, ".codex") + require.NoError(t, os.MkdirAll(codexDir, 0o750)) + configPath := filepath.Join(codexDir, configFileName) + require.NoError(t, os.WriteFile(configPath, []byte(content), 0o600)) + + ag := &CodexAgent{} + _, err := ag.InstallHooks(context.Background(), false, false) + require.NoError(t, err) + + data, err := os.ReadFile(configPath) + require.NoError(t, err) + require.Equal(t, content, string(data), "config.toml with user content must be left untouched") + }) + } } // assertHookCommand verifies that one of the hook entries in groups contains the expected command. From 205486db1f1d989d48ee81344ef58503c4d55f3e Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Wed, 15 Jul 2026 11:04:48 +0200 Subject: [PATCH 2/4] docs(codex): drop obsolete .codex/config.toml flag instructions Co-Authored-By: Claude Fable 5 Entire-Checkpoint: 01KXJGB1E6VHX5C88MDB70KSG0 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cbc131db2d..cbb6b75474 100644 --- a/README.md +++ b/README.md @@ -452,7 +452,7 @@ Local settings override project settings field-by-field. When you run `entire st ### Agent-Specific Steps & Limitations -- When enabling Entire for Codex, the command will also create or update `.codex/config.toml` with `hooks = true` to enable Codex hooks. If you configure Codex manually, make sure this flag is set in your `.codex/config.toml`. Or select Codex from the interactive agent picker when running `entire enable`. +- Codex hooks are enabled by default (codex-cli 0.124.0+), so enabling Entire for Codex only installs `.codex/hooks.json` — no `config.toml` is needed. A leftover `.codex/config.toml` written by older Entire versions is cleaned up automatically on the next `entire enable`, as long as it contains nothing but the old feature flag. - Entire supports Cursor IDE and Cursor Agent CLI tool, but `entire rewind` is not available at this time. Other commands (`doctor`, `status` etc.) work the same as all other agents. - Entire supports Copilot CLI, but not Copilot in VS Code, in other IDEs, or on github.com. - Entire supports Pi coding agent (Preview). Pi uses a TypeScript extension instead of a JSON hook config. Subagent capture is not currently available. From 97b7f26f02e80c6c6a7ebab21437cb99b76abc50 Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Wed, 15 Jul 2026 11:06:56 +0200 Subject: [PATCH 3/4] test(codex): rename CreatesConfig test, install writes hooks.json only Co-Authored-By: Claude Fable 5 Entire-Checkpoint: 01KXJGEY2ZZRE25AK3J4HEF1DV --- cmd/entire/cli/agent/codex/hooks_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/entire/cli/agent/codex/hooks_test.go b/cmd/entire/cli/agent/codex/hooks_test.go index d9e9015423..f11fae418f 100644 --- a/cmd/entire/cli/agent/codex/hooks_test.go +++ b/cmd/entire/cli/agent/codex/hooks_test.go @@ -21,7 +21,7 @@ func setupTestEnv(t *testing.T) string { return tempDir } -func TestInstallHooks_CreatesConfig(t *testing.T) { +func TestInstallHooks_CreatesHooksJSONOnly(t *testing.T) { tempDir := setupTestEnv(t) ag := &CodexAgent{} From 4cd43ed8d6df5992ce4a5e9fafdf2959fe3d967a Mon Sep 17 00:00:00 2001 From: Victor Gutierrez Calderon Date: Wed, 15 Jul 2026 11:17:28 +0200 Subject: [PATCH 4/4] refactor(codex): drop stale config.toml self-heal, never touch the file The CLI no longer manages .codex/config.toml at all. A flag-only leftover from an older entire version is harmless outside /agents and must be removed manually inside it (documented in README and #842). Co-Authored-By: Claude Fable 5 Entire-Checkpoint: 01KXJH2786VR1NMASSNH42MDWG --- README.md | 2 +- cmd/entire/cli/agent/codex/hooks.go | 93 ++---------------------- cmd/entire/cli/agent/codex/hooks_test.go | 88 ++++------------------ 3 files changed, 21 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index cbb6b75474..155bf8eda2 100644 --- a/README.md +++ b/README.md @@ -452,7 +452,7 @@ Local settings override project settings field-by-field. When you run `entire st ### Agent-Specific Steps & Limitations -- Codex hooks are enabled by default (codex-cli 0.124.0+), so enabling Entire for Codex only installs `.codex/hooks.json` — no `config.toml` is needed. A leftover `.codex/config.toml` written by older Entire versions is cleaned up automatically on the next `entire enable`, as long as it contains nothing but the old feature flag. +- Codex hooks are enabled by default (codex-cli 0.124.0+), so enabling Entire for Codex only installs `.codex/hooks.json` — no `config.toml` is needed and Entire never creates one. If an older Entire version left a `.codex/config.toml` behind and your repo lives inside `~/.codex/agents`, delete that file to stop Codex's "malformed agent role definition" startup warning. - Entire supports Cursor IDE and Cursor Agent CLI tool, but `entire rewind` is not available at this time. Other commands (`doctor`, `status` etc.) work the same as all other agents. - Entire supports Copilot CLI, but not Copilot in VS Code, in other IDEs, or on github.com. - Entire supports Pi coding agent (Preview). Pi uses a TypeScript extension instead of a JSON hook config. Subagent capture is not currently available. diff --git a/cmd/entire/cli/agent/codex/hooks.go b/cmd/entire/cli/agent/codex/hooks.go index 8cf72094c5..13e258cce4 100644 --- a/cmd/entire/cli/agent/codex/hooks.go +++ b/cmd/entire/cli/agent/codex/hooks.go @@ -6,7 +6,6 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/entireio/cli/cmd/entire/cli/agent" "github.com/entireio/cli/cmd/entire/cli/jsonutil" @@ -118,11 +117,6 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool } if count == 0 { - // Still self-heal a stale feature-flag config.toml left by an - // older entire version, even if hooks were already present. - if err := cleanupStaleFeatureConfig(repoRoot); err != nil { - return 0, err - } return 0, nil } @@ -158,13 +152,11 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool return 0, fmt.Errorf("failed to write hooks.json: %w", err) } - // Hooks are enabled by default in Codex (since 0.124.0), so no feature - // flag is written. Self-heal any stale .codex/config.toml an older - // entire version left behind. - if err := cleanupStaleFeatureConfig(repoRoot); err != nil { - return count, err - } - + // No .codex/config.toml is written: hooks are enabled by default in + // Codex (since 0.124.0), and a TOML file inside Codex's reserved + // /agents tree would be rejected by its agent-role scanner + // at every startup (entireio/cli#842). A leftover config.toml written + // by an older entire version must be removed manually. return count, nil } @@ -359,78 +351,3 @@ func removeEntireHooks(groups []MatcherGroup) []MatcherGroup { } return result } - -// configFileName is the Codex config file name. -const configFileName = "config.toml" - -// featureLine / legacyFeatureLine are the TOML feature-flag lines older -// entire versions wrote to a project-local .codex/config.toml back when -// Codex hooks were experimental (the flag was renamed from `codex_hooks` -// to `hooks` in Codex 0.129.0). Hooks are enabled by default since Codex -// 0.124.0 (openai/codex#19012), so the flag is no longer written — these -// constants only identify stale files for cleanupStaleFeatureConfig. -const ( - featureLine = "hooks = true" - legacyFeatureLine = "codex_hooks = true" -) - -// cleanupStaleFeatureConfig removes a project-local .codex/config.toml left -// behind by an older entire version that wrote the hooks feature flag there. -// The flag is obsolete (hooks are on by default since Codex 0.124.0), and a -// leftover config.toml is actively harmful when the repo lives inside -// /agents — Codex recursively scans that tree for agent-role -// TOML files and rejects the leftover at every startup as a "malformed -// agent role definition" (entireio/cli#842). Only removes the file when -// every non-blank line is one of the exact feature-flag lines or the -// [features] header this package used to write (see -// isEntireManagedLocalConfig) — a file carrying any unrelated user content -// is left alone. -func cleanupStaleFeatureConfig(repoRoot string) error { - configPath := filepath.Join(repoRoot, ".codex", configFileName) - - data, err := os.ReadFile(configPath) //nolint:gosec // path constructed from repo root - if err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("failed to read stale config.toml: %w", err) - } - - if !isEntireManagedLocalConfig(string(data)) { - return nil - } - if err := os.Remove(configPath); err != nil { - return fmt.Errorf("failed to remove stale config.toml: %w", err) - } - return nil -} - -// isEntireManagedLocalConfig reports whether content consists of nothing but -// the [features] header and feature-flag lines this package used to write -// (plus blank lines), with at least one such managed line present. Any other -// non-blank line — a user's own setting or a comment — makes the file -// unmanaged, so cleanup leaves it untouched. -// -// The check is line-anchored on purpose: a substring scan could mistake a -// user's `webhooks = true` for our `hooks = true`, or match `[features]` -// inside an unrelated value. A whole-line match cannot mistake a user's -// line for ours. -func isEntireManagedLocalConfig(content string) bool { - managed := map[string]bool{ - "[features]": true, - featureLine: true, - legacyFeatureLine: true, - } - sawManaged := false - for raw := range strings.SplitSeq(content, "\n") { - trimmed := strings.TrimSpace(raw) - if trimmed == "" { - continue - } - if !managed[trimmed] { - return false - } - sawManaged = true - } - return sawManaged -} diff --git a/cmd/entire/cli/agent/codex/hooks_test.go b/cmd/entire/cli/agent/codex/hooks_test.go index f11fae418f..45ecc1ea18 100644 --- a/cmd/entire/cli/agent/codex/hooks_test.go +++ b/cmd/entire/cli/agent/codex/hooks_test.go @@ -46,7 +46,7 @@ func TestInstallHooks_CreatesHooksJSONOnly(t *testing.T) { // written. A TOML file there is actively harmful when the repo lives // inside /agents, where Codex's agent-role scanner rejects // it at startup (entireio/cli#842). - projectConfig := filepath.Join(tempDir, ".codex", configFileName) + projectConfig := filepath.Join(tempDir, ".codex", "config.toml") _, err = os.Stat(projectConfig) require.True(t, os.IsNotExist(err), "install must not create .codex/config.toml") } @@ -349,93 +349,35 @@ func TestInstallHooks_DoesNotModifyUserConfig(t *testing.T) { require.NoError(t, os.MkdirAll(codexHome, 0o750)) existingConfig := "model = \"gpt-4.1\"\n" - require.NoError(t, os.WriteFile(filepath.Join(codexHome, configFileName), []byte(existingConfig), 0o600)) + require.NoError(t, os.WriteFile(filepath.Join(codexHome, "config.toml"), []byte(existingConfig), 0o600)) ag := &CodexAgent{} _, err := ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) - configData, err := os.ReadFile(filepath.Join(codexHome, configFileName)) + configData, err := os.ReadFile(filepath.Join(codexHome, "config.toml")) require.NoError(t, err) require.Contains(t, string(configData), "model = \"gpt-4.1\"") require.NotContains(t, string(configData), `trust_level = "trusted"`) } -// TestInstallHooks_CleansUpStaleFeatureConfig pins the self-heal: a -// project-local .codex/config.toml containing only the feature-flag content -// older entire versions wrote (either the current `hooks = true` form or the -// legacy `codex_hooks = true` form) is removed on the next install. Hooks -// are enabled by default in Codex, and a leftover TOML file under -// /agents triggers a "malformed agent role definition" warning -// at every Codex startup (entireio/cli#842). -func TestInstallHooks_CleansUpStaleFeatureConfig(t *testing.T) { - staleContents := map[string]string{ - "current form": "[features]\nhooks = true\n", - "legacy form": "[features]\ncodex_hooks = true\n", - "both forms": "[features]\ncodex_hooks = true\nhooks = true\n", - "extra blanks": "\n[features]\n\nhooks = true\n\n", +// TestInstallHooks_LeavesExistingLocalConfigUntouched pins that install +// never reads, rewrites, or deletes a project-local .codex/config.toml — +// whether it's a user's own file or a feature-flag leftover from an older +// entire version. The CLI no longer manages that file at all; leftovers +// under /agents must be removed manually (entireio/cli#842). +func TestInstallHooks_LeavesExistingLocalConfigUntouched(t *testing.T) { + contents := map[string]string{ + "old entire leftover": "[features]\nhooks = true\n", + "user file": "model = \"gpt-4.1\"\n", } - for name, content := range staleContents { + for name, content := range contents { t.Run(name, func(t *testing.T) { tempDir := setupTestEnv(t) codexDir := filepath.Join(tempDir, ".codex") require.NoError(t, os.MkdirAll(codexDir, 0o750)) - configPath := filepath.Join(codexDir, configFileName) - require.NoError(t, os.WriteFile(configPath, []byte(content), 0o600)) - - ag := &CodexAgent{} - _, err := ag.InstallHooks(context.Background(), false, false) - require.NoError(t, err) - - _, err = os.Stat(configPath) - require.True(t, os.IsNotExist(err), "stale entire-managed config.toml must be removed") - }) - } -} - -// TestInstallHooks_CleansUpStaleFeatureConfig_WhenHooksAlreadyInstalled pins -// that the self-heal also runs on a re-enable where hooks.json is already -// up to date (the count == 0 path), so upgrading entire fixes the leftover -// without requiring a force reinstall. -func TestInstallHooks_CleansUpStaleFeatureConfig_WhenHooksAlreadyInstalled(t *testing.T) { - tempDir := setupTestEnv(t) - - ag := &CodexAgent{} - count, err := ag.InstallHooks(context.Background(), false, false) - require.NoError(t, err) - require.Equal(t, 4, count) - - configPath := filepath.Join(tempDir, ".codex", configFileName) - require.NoError(t, os.WriteFile(configPath, []byte("[features]\nhooks = true\n"), 0o600)) - - count, err = ag.InstallHooks(context.Background(), false, false) - require.NoError(t, err) - require.Equal(t, 0, count) - - _, err = os.Stat(configPath) - require.True(t, os.IsNotExist(err), "stale config.toml must be removed even when hooks.json is already current") -} - -// TestInstallHooks_PreservesUserLocalConfig pins that cleanup never deletes a -// .codex/config.toml carrying anything beyond the exact feature-flag content -// this package used to write — a user's own settings, comments, or lines that -// merely contain our tokens as substrings (`webhooks = true`) make the file -// unmanaged and it is left byte-identical. -func TestInstallHooks_PreservesUserLocalConfig(t *testing.T) { - userContents := map[string]string{ - "user setting alongside flag": "[features]\nhooks = true\nsandbox = \"workspace-write\"\n", - "comment": "# my codex config\n[features]\nhooks = true\n", - "substring lookalike": "[features]\nwebhooks = true\n", - "unrelated file": "model = \"gpt-4.1\"\n", - } - for name, content := range userContents { - t.Run(name, func(t *testing.T) { - tempDir := setupTestEnv(t) - - codexDir := filepath.Join(tempDir, ".codex") - require.NoError(t, os.MkdirAll(codexDir, 0o750)) - configPath := filepath.Join(codexDir, configFileName) + configPath := filepath.Join(codexDir, "config.toml") require.NoError(t, os.WriteFile(configPath, []byte(content), 0o600)) ag := &CodexAgent{} @@ -444,7 +386,7 @@ func TestInstallHooks_PreservesUserLocalConfig(t *testing.T) { data, err := os.ReadFile(configPath) require.NoError(t, err) - require.Equal(t, content, string(data), "config.toml with user content must be left untouched") + require.Equal(t, content, string(data), "install must not touch an existing .codex/config.toml") }) } }