Skip to content

Commit ac2787b

Browse files
committed
fix: git worktree fallback in shared credential resolution
The MCP server and hooks resolve credentials via resolve_credentials(cwd) which walks up from CWD. Codex worktrees (e.g., ~/.codex/worktrees/xyz/repo) never reach the project's .cems/credentials. Added _resolve_main_worktree() fallback to both shared/credentials.py and hooks/utils/credentials.py. This is the same fix applied to the daemon's CredentialResolver in v0.11.4, now applied to the shared module so ALL consumers benefit (MCP, hooks, CLI).
1 parent 99b4a3e commit ac2787b

File tree

3 files changed

+61
-1
lines changed

3 files changed

+61
-1
lines changed

hooks/utils/credentials.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from __future__ import annotations
1919

2020
import os
21+
import subprocess
2122
from pathlib import Path
2223

2324
_HOME = str(Path.home().resolve()) # Resolve symlinks for consistent walk-up comparison
@@ -72,10 +73,30 @@ def _find_project_credentials(cwd: str) -> str | None:
7273
return None
7374

7475

76+
def _resolve_main_worktree(cwd: str) -> str | None:
77+
"""If CWD is inside a git worktree, return the main worktree path."""
78+
try:
79+
result = subprocess.run(
80+
["git", "-C", cwd, "worktree", "list", "--porcelain"],
81+
capture_output=True, text=True, timeout=3,
82+
)
83+
if result.returncode == 0:
84+
for line in result.stdout.splitlines():
85+
if line.startswith("worktree "):
86+
main_path = line[9:]
87+
if main_path != cwd:
88+
return main_path
89+
break
90+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
91+
pass
92+
return None
93+
94+
7595
def resolve_credentials(cwd: str | None = None) -> dict[str, str]:
7696
"""Resolve CEMS credentials with full precedence chain.
7797
7898
1. Per-project .cems/credentials (walk up from CWD, stop before $HOME)
99+
1b. If CWD is a git worktree, try the main worktree path
79100
2. Environment variables (both CEMS_API_URL and CEMS_API_KEY must be set)
80101
3. Global ~/.cems/credentials (fallback)
81102
@@ -87,6 +108,13 @@ def resolve_credentials(cwd: str | None = None) -> dict[str, str]:
87108
if project_path:
88109
return _parse_credentials_file(project_path)
89110

111+
# 1b. Git worktree fallback (e.g., Codex worktrees)
112+
main_wt = _resolve_main_worktree(cwd)
113+
if main_wt:
114+
project_path = _find_project_credentials(main_wt)
115+
if project_path:
116+
return _parse_credentials_file(project_path)
117+
90118
# 2. Check env vars — require BOTH URL and key to be set.
91119
# Partial env (e.g., URL in env, key in file) is intentionally not supported
92120
# to avoid silently mixing credentials from different sources.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "cems"
3-
version = "0.12.0"
3+
version = "0.12.1"
44
description = "Continuous Evolving Memory System - Dual-layer memory with scheduled maintenance"
55
readme = "README.md"
66
requires-python = ">=3.11"

src/cems/shared/credentials.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from __future__ import annotations
1212

1313
import os
14+
import subprocess
1415
from pathlib import Path
1516

1617
_HOME = str(Path.home().resolve()) # Resolve symlinks for consistent walk-up comparison
@@ -59,10 +60,34 @@ def find_project_credentials(cwd: str) -> str | None:
5960
return None
6061

6162

63+
def _resolve_main_worktree(cwd: str) -> str | None:
64+
"""If CWD is inside a git worktree, return the main worktree path.
65+
66+
Handles Codex worktrees: CWD like ~/.codex/worktrees/e8c3/repo
67+
maps back to the main clone (e.g., ~/Development/repo).
68+
"""
69+
try:
70+
result = subprocess.run(
71+
["git", "-C", cwd, "worktree", "list", "--porcelain"],
72+
capture_output=True, text=True, timeout=3,
73+
)
74+
if result.returncode == 0:
75+
for line in result.stdout.splitlines():
76+
if line.startswith("worktree "):
77+
main_path = line[9:]
78+
if main_path != cwd:
79+
return main_path
80+
break
81+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
82+
pass
83+
return None
84+
85+
6286
def resolve_credentials(cwd: str | None = None) -> dict[str, str]:
6387
"""Resolve CEMS credentials with full precedence chain.
6488
6589
1. Per-project .cems/credentials (walk up from CWD, stop before $HOME)
90+
1b. If CWD is a git worktree, try the main worktree path
6691
2. Environment variables (both CEMS_API_URL and CEMS_API_KEY must be set)
6792
3. Global ~/.cems/credentials (fallback)
6893
@@ -74,6 +99,13 @@ def resolve_credentials(cwd: str | None = None) -> dict[str, str]:
7499
if project_path:
75100
return parse_credentials_file(project_path)
76101

102+
# 1b. Git worktree fallback (e.g., Codex worktrees)
103+
main_wt = _resolve_main_worktree(cwd)
104+
if main_wt:
105+
project_path = find_project_credentials(main_wt)
106+
if project_path:
107+
return parse_credentials_file(project_path)
108+
77109
# 2. Check env vars — require BOTH URL and key
78110
env_url = os.environ.get("CEMS_API_URL", "")
79111
env_key = os.environ.get("CEMS_API_KEY", "")

0 commit comments

Comments
 (0)