Skip to content

Commit f3cb6f7

Browse files
authored
Merge trail: fix(codex): stop writing .codex/config.toml — hooks are on by default
fix(codex): stop writing .codex/config.toml — hooks are on by default
2 parents 2f67daa + c1a62d6 commit f3cb6f7

3 files changed

Lines changed: 44 additions & 128 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -502,7 +502,7 @@ Local settings override project settings field-by-field. When you run `entire st
502502

503503
### Agent-Specific Steps & Limitations
504504

505-
- 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`.
505+
- 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.
506506
- 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.
507507
- Entire supports Copilot CLI, but not Copilot in VS Code, in other IDEs, or on github.com.
508508
- Entire supports Pi coding agent (Preview). Pi uses a TypeScript extension instead of a JSON hook config. Subagent capture is not currently available.

cmd/entire/cli/agent/codex/hooks.go

Lines changed: 5 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9-
"strings"
109

1110
"github.com/entireio/cli/cmd/entire/cli/agent"
1211
"github.com/entireio/cli/cmd/entire/cli/jsonutil"
@@ -118,11 +117,6 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool
118117
}
119118

120119
if count == 0 {
121-
// Still ensure the feature flag is configured even if hooks
122-
// were already present (e.g., manually installed).
123-
if err := ensureProjectFeatureEnabled(repoRoot); err != nil {
124-
return 0, fmt.Errorf("failed to enable codex_hooks feature: %w", err)
125-
}
126120
return 0, nil
127121
}
128122

@@ -158,12 +152,11 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool
158152
return 0, fmt.Errorf("failed to write hooks.json: %w", err)
159153
}
160154

161-
// Enable the codex_hooks feature in the project-level .codex/config.toml.
162-
// This keeps the feature flag per-repo rather than global.
163-
if err := ensureProjectFeatureEnabled(repoRoot); err != nil {
164-
return count, fmt.Errorf("failed to enable codex_hooks feature: %w", err)
165-
}
166-
155+
// No .codex/config.toml is written: hooks are enabled by default in
156+
// Codex (since 0.124.0), and a TOML file inside Codex's reserved
157+
// <CODEX_HOME>/agents tree would be rejected by its agent-role scanner
158+
// at every startup (entireio/cli#842). A leftover config.toml written
159+
// by an older entire version must be removed manually.
167160
return count, nil
168161
}
169162

@@ -358,83 +351,3 @@ func removeEntireHooks(groups []MatcherGroup) []MatcherGroup {
358351
}
359352
return result
360353
}
361-
362-
// configFileName is the Codex config file name.
363-
const configFileName = "config.toml"
364-
365-
// featureLine is the TOML line that enables the hooks feature. The flag was
366-
// renamed from `codex_hooks` to `hooks` in Codex 0.129.0; the old name is
367-
// still accepted as a legacy alias but emits a deprecation warning at
368-
// every startup. ensureProjectFeatureEnabled rewrites the legacy form when
369-
// it sees it.
370-
const (
371-
featureLine = "hooks = true"
372-
legacyFeatureLine = "codex_hooks = true"
373-
)
374-
375-
// ensureProjectFeatureEnabled writes features.hooks = true to the
376-
// project-level .codex/config.toml. This keeps the feature flag per-repo.
377-
// Replaces the deprecated codex_hooks = true line if it's present.
378-
func ensureProjectFeatureEnabled(repoRoot string) error {
379-
configPath := filepath.Join(repoRoot, ".codex", configFileName)
380-
381-
data, err := os.ReadFile(configPath) //nolint:gosec // path constructed from repo root
382-
if err != nil && !os.IsNotExist(err) {
383-
return fmt.Errorf("failed to read config.toml: %w", err)
384-
}
385-
386-
content := string(data)
387-
hasNew := containsFeatureLine(content, featureLine)
388-
hasLegacy := containsFeatureLine(content, legacyFeatureLine)
389-
switch {
390-
case hasNew && hasLegacy:
391-
content = stripLegacyFeatureLine(content)
392-
case hasNew:
393-
return nil
394-
case hasLegacy:
395-
content = strings.Replace(content, legacyFeatureLine, featureLine, 1)
396-
case strings.Contains(content, "[features]"):
397-
content = strings.Replace(content, "[features]", "[features]\n"+featureLine, 1)
398-
default:
399-
if len(content) > 0 && !strings.HasSuffix(content, "\n") {
400-
content += "\n"
401-
}
402-
content += "\n[features]\n" + featureLine + "\n"
403-
}
404-
405-
if err := os.MkdirAll(filepath.Dir(configPath), 0o750); err != nil {
406-
return fmt.Errorf("failed to create .codex directory: %w", err)
407-
}
408-
if err := os.WriteFile(configPath, []byte(content), 0o600); err != nil { //nolint:gosec // path constructed from repo root
409-
return fmt.Errorf("failed to write config.toml: %w", err)
410-
}
411-
return nil
412-
}
413-
414-
// containsFeatureLine checks for an exact line match. A plain
415-
// strings.Contains is wrong because "hooks = true" is a substring of
416-
// "codex_hooks = true" — without the line-boundary anchor we'd treat the
417-
// legacy form as if the new form was already present.
418-
func containsFeatureLine(content, line string) bool {
419-
for _, raw := range strings.Split(content, "\n") {
420-
if strings.TrimSpace(raw) == line {
421-
return true
422-
}
423-
}
424-
return false
425-
}
426-
427-
// stripLegacyFeatureLine removes the deprecated `codex_hooks = true` line
428-
// from a TOML config string, dropping a trailing blank line so the file
429-
// stays tidy. The new `hooks = true` is added separately by the caller.
430-
func stripLegacyFeatureLine(content string) string {
431-
idx := strings.Index(content, legacyFeatureLine)
432-
if idx < 0 {
433-
return content
434-
}
435-
end := idx + len(legacyFeatureLine)
436-
if end < len(content) && content[end] == '\n' {
437-
end++
438-
}
439-
return content[:idx] + content[end:]
440-
}

cmd/entire/cli/agent/codex/hooks_test.go

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ func setupTestEnv(t *testing.T) string {
2121
return tempDir
2222
}
2323

24-
func TestInstallHooks_CreatesConfig(t *testing.T) {
24+
func TestInstallHooks_CreatesHooksJSONOnly(t *testing.T) {
2525
tempDir := setupTestEnv(t)
2626

2727
ag := &CodexAgent{}
@@ -42,14 +42,13 @@ func TestInstallHooks_CreatesConfig(t *testing.T) {
4242
assertHookCommand(t, hooksFile.Hooks.Stop, agentpkg.WrapProductionSilentHookCommand("entire hooks codex stop"), "Stop")
4343
assertHookCommand(t, hooksFile.Hooks.PostToolUse, agentpkg.WrapProductionSilentHookCommand("entire hooks codex post-tool-use"), "PostToolUse")
4444

45-
// Verify project-level config.toml enables the hooks feature (per-repo)
46-
projectConfig := filepath.Join(tempDir, ".codex", configFileName)
47-
projectData, err := os.ReadFile(projectConfig)
48-
require.NoError(t, err)
49-
require.Contains(t, string(projectData), "hooks = true")
50-
require.NotContains(t, string(projectData), "codex_hooks = true",
51-
"deprecated codex_hooks line must not be written by fresh installs")
52-
require.Contains(t, string(projectData), "[features]")
45+
// Hooks are enabled by default in Codex, so no .codex/config.toml is
46+
// written. A TOML file there is actively harmful when the repo lives
47+
// inside <CODEX_HOME>/agents, where Codex's agent-role scanner rejects
48+
// it at startup (entireio/cli#842).
49+
projectConfig := filepath.Join(tempDir, ".codex", "config.toml")
50+
_, err = os.Stat(projectConfig)
51+
require.True(t, os.IsNotExist(err), "install must not create .codex/config.toml")
5352
}
5453

5554
func TestInstallHooks_WindowsWrapperProbeSuccessKeepsWrappedCommands(t *testing.T) {
@@ -350,42 +349,46 @@ func TestInstallHooks_DoesNotModifyUserConfig(t *testing.T) {
350349

351350
require.NoError(t, os.MkdirAll(codexHome, 0o750))
352351
existingConfig := "model = \"gpt-4.1\"\n"
353-
require.NoError(t, os.WriteFile(filepath.Join(codexHome, configFileName), []byte(existingConfig), 0o600))
352+
require.NoError(t, os.WriteFile(filepath.Join(codexHome, "config.toml"), []byte(existingConfig), 0o600))
354353

355354
ag := &CodexAgent{}
356355
_, err := ag.InstallHooks(context.Background(), false, false)
357356
require.NoError(t, err)
358357

359-
configData, err := os.ReadFile(filepath.Join(codexHome, configFileName))
358+
configData, err := os.ReadFile(filepath.Join(codexHome, "config.toml"))
360359
require.NoError(t, err)
361360
require.Contains(t, string(configData), "model = \"gpt-4.1\"")
362361
require.NotContains(t, string(configData), `trust_level = "trusted"`)
363362
}
364363

365-
// TestInstallHooks_RewritesLegacyFeatureLine pins the rule that an existing
366-
// `codex_hooks = true` line — written by older entire CLI versions — must
367-
// be rewritten to the new `hooks = true` form on the next install. Codex
368-
// 0.129.0 still accepts the legacy alias but prints a deprecation warning
369-
// at every startup; rewriting silences it without forcing the user to
370-
// touch their .codex/config.toml.
371-
func TestInstallHooks_RewritesLegacyFeatureLine(t *testing.T) {
372-
tempDir := setupTestEnv(t)
373-
374-
codexDir := filepath.Join(tempDir, ".codex")
375-
require.NoError(t, os.MkdirAll(codexDir, 0o750))
376-
existingConfig := "[features]\ncodex_hooks = true\n"
377-
configPath := filepath.Join(codexDir, configFileName)
378-
require.NoError(t, os.WriteFile(configPath, []byte(existingConfig), 0o600))
379-
380-
ag := &CodexAgent{}
381-
_, err := ag.InstallHooks(context.Background(), false, false)
382-
require.NoError(t, err)
383-
384-
configData, err := os.ReadFile(configPath)
385-
require.NoError(t, err)
386-
require.Contains(t, string(configData), "hooks = true")
387-
require.NotContains(t, string(configData), "codex_hooks = true",
388-
"legacy codex_hooks line must be replaced, not left alongside the new form")
364+
// TestInstallHooks_LeavesExistingLocalConfigUntouched pins that install
365+
// never reads, rewrites, or deletes a project-local .codex/config.toml —
366+
// whether it's a user's own file or a feature-flag leftover from an older
367+
// entire version. The CLI no longer manages that file at all; leftovers
368+
// under <CODEX_HOME>/agents must be removed manually (entireio/cli#842).
369+
func TestInstallHooks_LeavesExistingLocalConfigUntouched(t *testing.T) {
370+
contents := map[string]string{
371+
"old entire leftover": "[features]\nhooks = true\n",
372+
"user file": "model = \"gpt-4.1\"\n",
373+
}
374+
for name, content := range contents {
375+
t.Run(name, func(t *testing.T) {
376+
tempDir := setupTestEnv(t)
377+
378+
codexDir := filepath.Join(tempDir, ".codex")
379+
require.NoError(t, os.MkdirAll(codexDir, 0o750))
380+
configPath := filepath.Join(codexDir, "config.toml")
381+
require.NoError(t, os.WriteFile(configPath, []byte(content), 0o600))
382+
383+
ag := &CodexAgent{}
384+
_, err := ag.InstallHooks(context.Background(), false, false)
385+
require.NoError(t, err)
386+
387+
data, err := os.ReadFile(configPath)
388+
require.NoError(t, err)
389+
require.Equal(t, content, string(data), "install must not touch an existing .codex/config.toml")
390+
})
391+
}
389392
}
390393

391394
// assertHookCommand verifies that one of the hook entries in groups contains the expected command.

0 commit comments

Comments
 (0)