Skip to content

chore(git): ignore .Codex/, where Codex puts its worktrees - #601

Merged
cdeust merged 1 commit into
mainfrom
chore/ignore-codex-worktrees
Sep 16, 2026
Merged

cdeust merged 1 commit into
mainfrom
chore/ignore-codex-worktrees

Conversation

@cdeust

@cdeust cdeust commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

The project's standing rule puts every git worktree at .claude/worktrees/<name>/, a path .gitignore already covers. Codex uses .Codex/worktrees/ instead, and nothing ignored it: reviewing PR #600 found a sixty-entry checkout sitting at .Codex/worktrees/shared-host-decisions, tracked by nothing and one git add -A away from being committed into the repository.

This ignores .Codex/. It does not move the worktree, which is the owner's call and unrelated to whether the path can be committed by accident.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Test plan

  • Created .Codex/worktrees/probe/file.txt in this worktree: git status --porcelain --untracked-files=all lists zero .Codex entries, and git check-ignore -v names .gitignore:66 as the matching rule. Removed the probe afterwards.
  • No source file changed, so the suite is unaffected.

Audit notes

  • The rule sits next to the existing .claude/* block, with the observation that produced it, so the next reader knows which tool creates that path and why it is ignored rather than moved.

Coding-standards compliance

  • §9 No dead code, no TODO.

Breaking changes

None. A path that was never tracked stops being offered as untracked.

Reviewer checklist

  • CHANGELOG.md updated: not applicable, this changes no behaviour of the shipped package.
  • No secrets or PII in the diff.
  • CI passes on the latest commit.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

The project's rule puts every worktree at .claude/worktrees/<name>/, which
.gitignore already covers. Codex uses .Codex/worktrees/ instead, and that
path was tracked: reviewing PR #600 found a 60-entry checkout sitting
there, one `git add -A` away from being committed.

Verified with a probe file under .Codex/worktrees/: untracked before,
matched by the rule after (`git check-ignore -v` names .gitignore:66).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
@cdeust

cdeust commented Sep 16, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: APPROVE

Reviewed 8ccdb27 from a detached worktree at .claude/worktrees/review-601, base origin/main 0fc6bd1. Nothing edited, nothing pushed.

Stakes (Move 7)

Low. One repository-config file, five added lines, no source, no public surface, no security path, no test impact. Moves 1, 3 and 6 still run, since they run at every stakes level. Moves 2, 4 and 5 are informal here because there is no unit to audit.

Move 0, ledger and seen-defect check

The diff has exactly one path: a .gitignore pattern. I re-enumerated it against the PR body and found no unmapped branch and no unclaimed effect.

One scope statement in the body ("It does not move the worktree, which is the owner's call") reads like a deferral, so I checked whether it dismisses a defect this diff touches. It does not. Where Codex writes its worktrees is host-tool configuration outside the repository tree, unreachable from this diff and from every check it ran, and the author names the deferral rather than hiding it. Not a section 14 bypass. There is a related note below under Non-blocking.

Verification, re-run independently rather than taken from the PR body

  1. The rule matches what it claims. I created .Codex/worktrees/probe/file.txt in the worktree. git status --porcelain --untracked-files=all returned 0 lines in total, with no .Codex entry. git check-ignore -v .Codex/worktrees/probe/file.txt returned .gitignore:66:.Codex/. I then removed the probe and the tree came back clean. The commit message's claim of .gitignore:66 is exact: the pattern sits on line 66.

  2. The rule is not broader than intended. git ls-files | grep -i codex returns 11 tracked paths (docs/codex-plugin.md, the plugins/hypermnesia-mcp-codex/ tree including .codex-plugin/plugin.json and .codexignore, and tests_py/scripts/test_codex_plugin_contract.py). I ran git check-ignore -v over each one and none is ignored, so no tracked path changes status. The .claude/ behaviour is untouched: .claude/worktrees/x and .claude/other.json still match .gitignore:60:.claude/*, and .claude/settings.json is still not ignored, so the ! exception on line 61 holds. The new rule sits after that negation and matches a disjoint prefix, so it cannot shadow it.

  3. The comment is four consecutive comment lines, 62 to 65. The longest comment run anywhere in .gitignore is 4, set by the pre-existing .claude block on lines 56 to 59, well under the decision gate's eight. The content is a measured observation rather than a claim, and I confirmed the observation itself: .Codex/worktrees/shared-host-decisions exists in the primary checkout with 61 paths at depth 3 or less, PR fix: align Claude and Codex backend selection and verify shared handoff #600 is open and is the review that surfaced it, and on main without this rule the directory appears as ?? .Codex/. The "one git add -A away" statement is literally true.

  4. Both gates pass on the branch, after uv sync --no-default-groups --extra dev --extra sqlite and with UV_NO_SYNC=1 uv run. scripts/check_doc_claims.py printed doc claims OK (1 declared not-a-claim exemption(s)) and exited 0. scripts/check_craftsmanship.py --base origin/main printed Craftsmanship gate: OK and exited 0.

  5. gh pr checks 601 reports 24 pass and 4 skipping (Tag a release, both Docker Build images, and the scheduled fuzz batch, all correctly inert for a non-source change). Nothing failing, nothing pending.

Moves 1, 3 and 6

Layer boundary: no import and no module, so nothing applies. Wiring and contract drift: no symbol added, renamed or deleted, and no caller to update; no commented-out code, no untracked TODO, no debug statement. Security and hygiene: no secret, no PII, no binary, no generated file; conventional commit (chore(git):), one logical change, 5 insertions and 0 deletions, so no scope creep. The change can only remove a path from git add reach, so it cannot cause anything to be committed.

Adversarial pass, four apertures, all examined

Over-fit: the pattern is unanchored, so it matches a .Codex directory at any depth rather than only the repository root. I verified this is harmless, since nothing tracked matches.

Missed cases: the intended path .Codex/worktrees/** is covered at every depth, and an unanchored pattern cannot under-match relative to an anchored one.

Robustness: an already-tracked file under .Codex/ would stay tracked despite the rule, but git ls-files shows none exists, so there is no silent-divergence case.

Test adequacy: there is no automated test, and none is warranted. The rule's behaviour is a single git check-ignore fact, which I reproduced rather than trusted. A .gitignore regression test here would be ceremony.

No blocking finding at any aperture.

Non-blocking

.gitignore:66 is unanchored. /.Codex/ would express the actual intent, a root-level tool directory, and would not silently swallow a stray .Codex nested deeper in the tree. Nothing today is affected; this is only about the rule saying what it means.

.gitignore:66 carries a case-fold asymmetry worth knowing about. This clone has core.ignorecase=true, the APFS default, under which .Codex/ also matches a lowercase .codex/; I confirmed that it does. On Linux CI it would not. If Codex ever emits .codex/, the rule works on the author's machine and not in CI. That is not a defect of this diff, only a fact to hold before relying on the rule for a lowercase path.

The deferral "it does not move the worktree" is a sound scope call, but the underlying condition, a worktree living outside the path the project rule names, is now invisible to git status rather than resolved. An issue would keep it tracked rather than merely silenced.

Verdict

APPROVE. The diff does what it says, its own commit message is reproducible down to the line number, it widens nothing that is tracked, .claude/ semantics are intact, both gates pass on the branch and CI is green.

@cdeust
cdeust merged commit e565d12 into main Sep 16, 2026
28 checks passed
@cdeust
cdeust deleted the chore/ignore-codex-worktrees branch September 16, 2026 21:10
cdeust added a commit that referenced this pull request Sep 17, 2026
…sourced to what names it

ADR-0693 records why the Codex plugin starts lean: the full surface is the
largest fixed token cost a session pays, not a startup-time limit. The
worktree row cites docs/agent-guidance.md for the Claude Code directory
only; the Codex directory is observed behaviour that #601 gitignored.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
cdeust added a commit that referenced this pull request Sep 17, 2026
…and Codex (#603)

* docs(readme): predictions and calibration, one store for Claude Code and Codex

The README described what Cortex remembers and never that it can record
what it expected (#597, ADR-1076): a section under "Keep context useful"
shows predict, resolve_prediction and calibration with their real return
shapes and says what the Brier score means against the 0.25 of a constant
0.5 forecast. The host table gains the row, and the Codex paragraph says
the plugin reads the same saved backend selection as the Claude launcher
(#600). The unreleased changelog had two "### Added" headings; one now.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): one column per host in the capability table

Codex was folded into "local stdio hosts" although its plugin starts the
server with --profile lean (ten named tools, no hooks) while a direct
codex mcp add gets the full surface. The table now has a column for each,
says which tools lean lacks (predictions, wiki writes, ingestion), where the
shared backend selection lives (#600), and which worktree directory doctor
checks per host.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): the calibration band the code can return, and the release hedge for Codex

BAND_EDGES steps by 0.2, so the illustrative reliability row shows
[0.6, 0.8]. The Codex paragraph names CORTEX_BACKEND between the two
variables it already named and says the shared backend selection reaches
the plugin with the release after 4.22.0, since the plugin launches the
published package.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): the worktree row states the convention, not a doctor check

The doctor check for worktree locations is PR #602, not merged.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* docs(readme): the lean profile's real reason, and each worktree cell sourced to what names it

ADR-0693 records why the Codex plugin starts lean: the full surface is the
largest fixed token cost a session pays, not a startup-time limit. The
worktree row cites docs/agent-guidance.md for the Claude Code directory
only; the Codex directory is observed behaviour that #601 gitignored.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

---------

Co-authored-by: Claude Fable 5.1 <[email protected]>
cdeust added a commit that referenced this pull request Sep 17, 2026
… (ADR-1078)

.gitignore's .Codex/ rule (#601) was unanchored, matching a .Codex
directory at any depth instead of just the repo root. Anchor to
/.Codex/.

The condition that rule hid: a git worktree living outside
.claude/worktrees/, the only location docs/agent-guidance.md:160-166
names, stays where the owner decided it stays, but is now reported
rather than silenced. mcp_server/doctor.py gains an optional,
non-blocking "worktree locations" check (surfaced through
python -m mcp_server.doctor, check_setup, and /preflight) that lists
any git worktree list --porcelain entry outside
<main-worktree>/.claude/worktrees/. The allowed root is derived from
the main worktree's own listing (always first, per git-worktree(1)
section list) rather than git rev-parse --show-toplevel, which
resolves to a linked worktree's own path and would false-positive on
every sibling worktree run from inside one.

Decision recorded as ADR-1078.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH
cdeust added a commit that referenced this pull request Sep 17, 2026
… (ADR-1078) (#602)

* fix(gitignore): anchor .Codex/ to repo root, report outside worktrees (ADR-1078)

.gitignore's .Codex/ rule (#601) was unanchored, matching a .Codex
directory at any depth instead of just the repo root. Anchor to
/.Codex/.

The condition that rule hid: a git worktree living outside
.claude/worktrees/, the only location docs/agent-guidance.md:160-166
names, stays where the owner decided it stays, but is now reported
rather than silenced. mcp_server/doctor.py gains an optional,
non-blocking "worktree locations" check (surfaced through
python -m mcp_server.doctor, check_setup, and /preflight) that lists
any git worktree list --porcelain entry outside
<main-worktree>/.claude/worktrees/. The allowed root is derived from
the main worktree's own listing (always first, per git-worktree(1)
section list) rather than git rev-parse --show-toplevel, which
resolves to a linked worktree's own path and would false-positive on
every sibling worktree run from inside one.

Decision recorded as ADR-1078.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* fix(doctor): split _worktree_locations to clear the craftsmanship method-size gate

CI's craftsmanship gate job was red on 217ec1f: _worktree_locations
spanned 51 lines (end_lineno - lineno), over the 40-line method cap
(scripts/craftsmanship_rules.py METHOD_LINE_LIMIT). Split the
classification logic into _worktree_classification(entries) -> (ok,
detail), leaving _worktree_locations as a short orchestrator that
turns the tuple into a Check. Same behavior, same tests.

python scripts/check_craftsmanship.py --base origin/main -> Craftsmanship gate: OK

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

* fix(doctor): honor host worktree roots and preserve Git paths (#602)

* test(doctor): verify worktree signals and report unavailable inspection (#602)

* fix(doctor): a project without git is not a failed inspection (#602)

_worktree_locations() treated every failure mode of `git worktree list`
(no repository, git missing, timeout) as the same "inspection unavailable"
WARN. Cortex is an MCP server whose cwd is whatever project the host
opened; a project with no Git repository is ordinary, not a failure to
inspect, so every such session's preflight showed a WARN whose fix
command (`git worktree list --porcelain -z`) can never succeed there.

`run_with_hard_timeout` collapses every subprocess failure mode to
`None`, so the two cases can't be told apart after the call. A new
`_is_git_checkout()` stdlib probe runs first instead — the same upward
walk for a `.git` entry that Git itself performs to discover a
repository, no subprocess involved. No `.git` anywhere up the tree now
reports `ok True, detail "not a git checkout"`; a `.git` present but Git
missing from PATH or the listing timing out keeps the optional WARN.

Renamed test_not_a_git_checkout_warns to test_not_a_git_checkout_passes
(now asserts the ok True case) and added
test_git_unavailable_in_a_checkout_warns for the WARN case with a real
.git present. Updated the parametrised None case in
test_unavailable_or_empty_listing_contract, whose message no longer
claims "not a Git checkout" as a possible cause since that case now
short-circuits before the subprocess call. ADR-1079's Consequences
section records the distinction.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cz7A6goxVzfKL9NT7k4zDH

---------

Co-authored-by: Claude Sonnet 5 <[email protected]>
@cdeust cdeust mentioned this pull request Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant