openai / openai/codex-plugin-cc
`/codex:transfer` is broken on Windows: false "did not record an imported thread" error, and no-arg auto-detection always fails
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 33.3k
- Forks
- 2.3k
- PR merge metrics
- No merged PRs in 30d
Description
/codex:transfer is broken on Windows: false "did not record an imported thread" error, and no-arg auto-detection always fails
Summary
On Windows, /codex:transfer never works out of the box, due to two independent bugs:
- No-arg auto-detection always fails with
Could not identify the current Claude transcript. Retry with --source <path-to-claude-jsonl>.— because Claude Code does not injectCODEX_COMPANION_TRANSCRIPT_PATHand there is no fallback. - Even with
--source, it reports a false failure:Codex reported that the Claude import completed, but did not record an imported thread.— even though the import succeeds and a valid record is written to~/.codex/external_agent_session_imports.json. Users never receive theircodex resume <id>line and silently accumulate one orphaned thread per attempt.
Environment
- Plugin:
codex@openai-codexv1.0.6 - Codex CLI:
codex-cli 0.144.2 - Node: v24.15.0
- OS: Windows 11 (x64)
Bug 1 — no-arg transcript auto-detection always fails on Windows
scripts/lib/claude-session-transfer.mjs → resolveClaudeSessionPath requires either --source or the CODEX_COMPANION_TRANSCRIPT_PATH env var, and throws otherwise:
const requestedPath = options.source || process.env[TRANSCRIPT_PATH_ENV];
if (!requestedPath) {
throw new Error("Could not identify the current Claude transcript. Retry with --source <path-to-claude-jsonl>.");
}
In practice the env var is not set for the plugin process, so bare /codex:transfer always fails. There is no fallback to locate the current transcript.
Suggested fix
Fall back to the most-recently-modified .jsonl under ~/.claude/projects (the current session's transcript is being actively appended to, so it is the newest file):
function findLatestTranscript() {
let projectsRoot;
try {
projectsRoot = fs.realpathSync(CLAUDE_PROJECTS_DIR);
} catch {
return null;
}
let best = null;
let bestMtime = -Infinity;
let dirents;
try {
dirents = fs.readdirSync(projectsRoot, { withFileTypes: true });
} catch {
return null;
}
for (const dirent of dirents) {
if (!dirent.isDirectory()) continue;
const dir = path.join(projectsRoot, dirent.name);
let files;
try {
files = fs.readdirSync(dir);
} catch {
continue;
}
for (const file of files) {
if (!file.endsWith(".jsonl")) continue;
const full = path.join(dir, file);
try {
const stat = fs.statSync(full);
if (stat.mtimeMs > bestMtime) {
bestMtime = stat.mtimeMs;
best = full;
}
} catch {
// ignore unreadable entries
}
}
}
return best;
}
export function resolveClaudeSessionPath(cwd, options = {}) {
const requestedPath = options.source || process.env[TRANSCRIPT_PATH_ENV];
const sourcePath = requestedPath
? resolveUserPath(cwd, requestedPath)
: findLatestTranscript();
if (!sourcePath) {
throw new Error("Could not identify the current Claude transcript. Retry with --source <path-to-claude-jsonl>.");
}
// …unchanged: .jsonl check, realpathSync, projects-dir containment check…
}
(A more precise variant could prefer the project dir whose name encodes cwd, but "newest transcript overall" reliably resolves to the live session.)
Bug 2 — post-import lookup never matches on Windows (false "did not record" error)
scripts/lib/codex.mjs → importedThreadIdForSource(sourcePath) looks up the record it just wrote using two equality checks that both fail on Windows:
const canonicalSource = fs.realpathSync(sourcePath);
const contentSha256 = sourceContentSha256(canonicalSource);
const match = records
.filter(
(record) =>
record?.source_path === canonicalSource && // (a)
record?.content_sha256 === contentSha256 && // (b)
typeof record?.imported_thread_id === "string"
)
.at(-1);
(a) Extended-length path prefix mismatch. The ledger (written by the Codex binary) stores source_path with the Windows \\?\ prefix, but fs.realpathSync() here returns the path without it, so the strings never match:
ledger source_path : \\?\C:\Users\<user>\.claude\projects\<proj>\<id>.jsonl
realpathSync() : C:\Users\<user>\.claude\projects\<proj>\<id>.jsonl
(b) Live-file content-hash race. The transcript .jsonl is a live, append-only file. content_sha256 is stored at import time, but this function re-hashes the file after the import — by then the transcript has grown (the transfer turn is written to it), so the hash differs.
Either alone forces a null result → the false error, despite the thread being created and recorded.
Suggested fix
Normalize paths before comparing (strip \\?\; on Windows unify separators + case), and treat the content hash as a preference, not a hard requirement:
const records = Array.isArray(ledger?.records) ? ledger.records : [];
const normalizePath = (p) => {
if (typeof p !== "string") return "";
let s = p.replace(/^\\\\\?\\/, "").replace(/^\/\/\?\//, "");
if (process.platform === "win32") s = s.replace(/\//g, "\\").toLowerCase();
return s;
};
const target = normalizePath(canonicalSource);
const bySource = records.filter(
(record) =>
normalizePath(record?.source_path) === target &&
typeof record?.imported_thread_id === "string"
);
// Transcript is live/append-only, so its hash can change between import and this
// lookup. Prefer an exact content-hash match, else the newest import.
const exact = bySource.filter((record) => record.content_sha256 === contentSha256).at(-1);
const match = exact ?? bySource.at(-1);
return match?.imported_thread_id ?? null;
Reproduction
- On Windows, run bare
/codex:transferfrom an active Claude Code session → Bug 1 ("Could not identify the current Claude transcript"). - Retry with
--source <path-to-jsonl>→ Bug 2 ("did not record an imported thread"). - Inspect
~/.codex/external_agent_session_imports.json: a new record with a validimported_thread_idis present, andsource_pathstarts with\\?\. codex resume <that id>works fine — confirming the import itself succeeded and both errors are false negatives.
Result after both fixes
Bare /codex:transfer on Windows auto-detects the current session, imports it, and prints the expected codex resume <id> line — no arguments and no false error.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in scripts/lib/claude-session-transfer.mjs at resolveClaudeSessionPath and scripts/lib/codex.mjs at importedThreadIdForSource. Reproduce both Windows cases from the issue, then verify that bare /codex:transfer finds the active transcript, --source recognizes the ledger record, and the command prints a working codex resume ID.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100