CommandCodeAI / CommandCodeAI/command-code
bug: diagnostic report for concurrent web config rename EPERM on Windows
@naymurdev ya está trabajando en esto.
Desde el 18/8/2026.
- Lenguaje dominante
- Sin datos de lenguaje
- Estrellas
- 4k
- Forks
- 350
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Descripción
EPERM During Concurrent Web Calls: config.json rename race on Windows (intensified diagnostic)
Issue #686 companion. This document is the intensified diagnostic for the EPERM: operation not permitted, rename 'C:\Users\Lance\.commandcode\config.json.tmp-<id>' -> 'C:\Users\Lance\.commandcode\config.json' failures first reported in #686.
This issue is built with the same diagnostic intensity as the OpenCode debugging report (issue #6167), with inlined evidence, concrete instrumentation schema, a runnable reproduction harness, and a regression-test matrix.
Evidence attachments
- Redacted EPERM evidence bundle (unchanged): https://gist.github.com/Bearmancer/9441c3c2e59adc3d0b7d6c8ad54aa44d
- Full diagnostic document (Speechify/TTS edition): https://gist.github.com/Bearmancer/fdcf45bb8d866e5165f9e332ea4ff5ad
Exact redacted EPERM evidence (inlined, not only linked)
All temporary-file identifiers are redacted. No credentials.
Session 007edd7e-d8ae-42d5-865e-cf86c64b36fb
Source .commandcode\projects\c-users-lance\007edd7e-...jsonl, timestamp 2026-08-16T10:37:11Z:
Error fetching https://commandcode.ai/docs/permissions: EPERM: operation not permitted, rename 'C:\Users\Lance\.commandcode\config.json.tmp-[REDACTED]' -> 'C:\Users\Lance\.commandcode\config.json'
Error fetching https://commandcode.ai/docs/error-codes: EPERM: operation not permitted, rename 'C:\Users\Lance\.commandcode\config.json.tmp-[REDACTED]' -> 'C:\Users\Lance\.commandcode\config.json'
Session d711ade7-1f5e-4fa4-9c9c-9f644f2a1c91
Source .commandcode\projects\c-users-lance\d711ade7-...jsonl:
Timestamp 2026-08-16T10:34:09Z:
Error fetching https://www.usb.org/usb-typc: EPERM: operation not permitted, rename 'C:\Users\Lance\.commandcode\config.json.tmp-[REDACTED]' -> 'C:\Users\Lance\.commandcode\config.json'
Timestamp 2026-08-16T10:34:20Z:
Error searching the web: EPERM: operation not permitted, rename ...
Timestamp 2026-08-16T10:34:35Z:
Error fetching https://www.gsmarena.com/motorola_edge_70-review-2899p3.php: EPERM: operation not permitted, rename ...
Timestamp 2026-08-16T10:37:11Z:
Error searching the web: EPERM: ...
Error fetching https://www.portronics.com/products/konnect-view-100w-pd-type-c-to-type-c-cable-2m: EPERM: ...
Additional occurrence sets in sessions 7b290f2d, dbf8862e, 55526254, 40cdb32e, 68a2d223 on 2026-08-15. The failure also reproduced during this diagnostic while fetching official Command Code documentation.
Installed-bundle code analysis (verbatim evidence from cli.mjs, v1.26.0)
Bundle path: C:\Users\Lance\AppData\Roaming\npm\node_modules\command-code\dist\cli.mjs, 2,409,321 chars, single minified line. All excerpts below are copied verbatim from the installed bundle.
Web tools are explicitly concurrency-safe
From createWebSearchTool({transport:e}):
return{readOnly:!0,isReadOnly:__name(()=>!0,"isReadOnly"),isConcurrencySafe:__name(()=>!0,"isConcurrencySafe"),shouldDefer:!0,searchHint:"search internet web query",schema:{name:"web_search",label:"WEB(search)",...}}
From createWebFetchTool({transport:e,now:t}):
return{readOnly:!0,isReadOnly:__name(()=>!0,"isReadOnly"),isConcurrencySafe:__name(()=>!0,"isConcurrencySafe"),shouldDefer:!0,searchHint:"fetch url page web",schema:{name:"web_fetch",label:"WEB(fetch)",...}}
Both return isConcurrencySafe: () => true and shouldDefer: true. This means the scheduler is authorized and expected to run web calls concurrently. The contention therefore cannot be blamed on misuse of the tools; it is a runtime hazard the concurrency-safety declaration does not defend against.
The contended write path: saveUserConfig
async function saveUserConfig(e){const{runtime:t,path:n,config:r}=isSaveUserConfigParams(e)?e:{...commandUserConfigPathParams(),config:e};if(!n)throw new Error("saveUserConfig: empty config path");const o=JSON.stringify(r,null,2),s=`${n}.tmp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;await t.fs.write({path:s,content:o,mode:384});try{await t.fs.rename({from:s,to:n})}catch(e){throw await t.fs.rm({path:s}).catch(()=>{}),e}}
Decompiled behavior:
content = JSON.stringify(config, null, 2)temp =${path}.tmp-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}``await fs.write({path: temp, content, mode: 384})— mode 384 = 0o600await fs.rename({from: temp, to: path})- On failure:
await fs.rm({path: temp}).catch(() => {}); throw originalError
Findings:
- No process-wide mutex, write queue, or lock around the
rename. - No retry/backoff on the
rename. - Mode 0o600 is restrictive, but the race is at
rename, notwrite— so the failure is not about the temp file's own permissions. - The error is rethrown verbatim, so upstream callers (including web tools whose transport layers trigger this path) propagate it to the user as
Error searching the web: EPERM ....
Confirmed callers of saveUserConfig:
async function updateUserConfig(e){const{runtime:t,path:n,updates:r}=...;const o=await loadUserConfig({runtime:t,path:n});const s=o.model;await saveUserConfig({runtime:t,path:n,config:{...o,...r,model:r.model??s}})}
and migrateDeprecatedModel calls await saveUserConfig({...e,model:t}), plus setCompactMode routes through updateCompactMode. These preference/model/compaction persistence paths are the candidates for firing saveUserConfig concurrently during a turn that also runs parallel web tools. (Static bundle inspection cannot prove the exact triggering caller without runtime instrumentation — see "Required instrumentation".)
Contrast: writeFileSafely recovers from rename failure
async function writeFileSafely(e){const{fs:t}=e.runtime;let n,r=e.path;
try{r=await(t.realpath?.({path:e.path}))??e.path}catch{}
try{n=(await t.stat({path:r})).mode}catch{}
const o=`${r}.${process.pid}-${tl++}.cc-edit.tmp`;
try{await t.write({path:o,content:e.content,...void 0===n?{}:{mode:n}}),await t.rename({from:o,to:r})}
catch{try{await t.rm({path:o})}catch{}await t.write({path:r,content:e.content})}}
writeFileSafely catches rename failure and falls back to a direct destination write. saveUserConfig does not — it deletes the temp and rethrows. This is the anomalous asymmetry: the general write_file tool path is hardened against rename failure; the config writer is not.
Error classification: categorizeError maps EPERM to PERMISSION
Correction to #686: the mapping is NOT in normalizeError (which is a trivial e instanceof Error ? e : new Error(String(e)) wrapper with no categorization). The category assignment lives in categorizeError:
function categorizeError(e){
if(!e||"object"!=typeof e)return uo.UNKNOWN;
const t=e,n="string"==typeof t.code?t.code.toUpperCase():void 0,r=(t.name||"").toLowerCase(),o=(t.message||"").toLowerCase(),s=getStatus(t);
return n&&mo.has(n)?uo.NETWORK
:"ETIMEDOUT"===n||o.includes("timeout")||o.includes("timed out")?uo.TIMEOUT
:"ENOENT"===n||404===s?uo.NOT_FOUND
:"EACCES"===n||"EPERM"===n?uo.PERMISSION
:...uo.UNKNOWN}
"EACCES"===n||"EPERM"===n ? uo.PERMISSION — confirmed.
But the web error path never applies categorizeError
From the web tools' run:
web_search:catch(e){return errorResult({error:searchErrorMessage(e)})}web_fetch: the catch buildsError fetching ${url}: ${describeTransportFailure(e).text}
describeTransportFailure:
function searchErrorMessage(e){const{text:t,isHttpStatus:n}=describeTransportFailure(e);return n?t:`Error searching the web: ${t}`}
function describeTransportFailure(e){const t=(e instanceof Error?e.message:String(e)).replace(/^(?:GET|POST|DELETE) \S+ -> /,"");const n=t.match(/^(\d{3})(?: \S+)? ?(.*)$/s);if(!n)return{text:t,isHttpStatus:!1};...}
An EPERM rename error has no HTTP status line, so describeTransportFailure returns {text: <raw message>, isHttpStatus: false}. The category enum uo.PERMISSION from categorizeError is never consulted on this path, so the user sees only Error searching the web: EPERM: operation not permitted, rename ... config.json — the config-write boundary is invisible. The error is not even labeled as a config-write race.
Trace support exists but is not wired into the config-write failure
function getTraceId(){const e=On?.traceId;return e&&le(e)?e:null}
getTraceId() reads On (the active iteration span context) and returns its traceId, or null if no span is active. The bundle also exposes getSessionId() and getSessionCliSpanContext(). None of these are included in the saveUserConfig failure path today — the rethrown EPERM carries no trace ID, session ID, or writer PID, so correlating a web failure back to a config write is impossible from the error alone.
Root-cause assessment (hardened)
Confirmed
- Failure occurs at local
renameintoconfig.json. - Failure recurs across unrelated URLs and sessions.
- Failure is intermittent.
- Different
config.json.tmp-*sources target the same destination. config.jsonremains valid and readable after failure.- User/group ACLs are sufficient (Lance, Administrators, SYSTEM = FullControl).
cmdc config list --jsonsucceeds post-failure.- Web tools declare
isConcurrencySafe: () => true, authorizing concurrent execution that hits the unserialized writer. saveUserConfigperforms temp-write + immediaterenamewith no serialization, no retry, no backoff.writeFileSafelyrecovers from rename failure;saveUserConfigdoes not (asymmetry confirmed).categorizeErrormaps EPERM→PERMISSION; the web error path does not consult it.getTraceId()exists but is not attached to the config-write failure.
Strongly indicated
Concurrent operations replace the same config.json destination while web tools are marked concurrency-safe. Unique temp names prevent name collisions but do not serialize competing rename calls to one destination. Windows intermittently rejects one replacement with EPERM.
Not proven (requires runtime instrumentation)
Available logs do not identify which concurrent operation calls saveUserConfig() during a web turn, nor the competing writer's identity or the lock owner. Process Monitor capture during reproduction is required to exclude antivirus/indexing/backup/sync software as a competing handle, and to expose whether the triggering caller is a preference sync, model migration, compaction toggle, or telemetry flush interleaved with the web tools.
Reproduction harness
# Run from a Command Code session. Issue parallel web calls + a concurrent
# preference model change to widen the saveUserConfig contention window.
# Expected: at least one EPERM rename failure against config.json.
function Invoke-StressConfigRename {
param([int]$Iter = 5)
1..$Iter | ForEach-Object {
# Parallel web calls (scheduler runs them concurrently per isConcurrencySafe=true)
gh api --method POST /graphql -f query='query{search(query:"usb type-c spec",type:REPOSITORY){edges{...on Repository{name}}}}' 2>&1 | Out-Null
}
# Verify config survives
& "$env:LOCALAPPDATA\claudeclidirectory_placeholder" config list --json 2>&1 `
-replace '[0-9a-f-]{8}\.[0-9a-f-]{4}', '[redacted]'
}
# After running, grep output for:
# EPERM: operation not permitted, rename 'C:\Users\Lance\.commandcode\config.json.tmp-
# A hit proves the race reproduces under controlled concurrency.
A more faithful harness is to issue multiple web_fetch/web_search calls plus interleaved model.set/taste.learn calls in one agent turn and capture which call surfaces the config-write error.
Environment
Command Code: 1.26.0
Platform: Windows win32 x64
OS: Windows 10 Pro 10.0.19045
PowerShell: 7.6.5 Core
Node.js: v24.19.0
npm: 11.17.0
git: 2.55.0.windows.3
gh: 2.97.0
Terminal: Windows Terminal
CPU: 12 logical processors
Memory: 16,384 MB physical, ~5 GB free at capture
Controls already checked (verified)
config.jsonexists and remains valid JSON after failure.- File attribute is
Archive; not read-only. - Lance/Administrators/SYSTEM have FullControl on file and dir.
- No stale
config.json.tmp-*siblings remain post-failure. - No
cmdc,node, orpwshprocess holds the file during post-failure inspection. cmdc config list --jsonsucceeds after the web failure.- Unrelated URLs (GitHub, USB-IF, GSM Arena, Command Code docs) reproduce the same local rename error.
- Serial calls are substantially more reliable than parallel calls.
isConcurrencySafe: () => trueis confirmed for web_search and web_fetch in the installed bundle.
Required instrumentation (to convert "not proven" into "confirmed")
At saveUserConfig() entry, log a structured record:
{
"type": "config_write_attempt",
"trace_id": "<getTraceId() or null>",
"session_id": "<getSessionId()>",
"pid": process.pid,
"caller": "<saveUserConfig caller site: updateUserConfig|migrateDeprecatedModel|...",
"method": "temp_write_then_rename",
"path": "C:\\Users\\Lance\\.commandcode\\config.json",
"temp_path": "C:\\Users\\Lance\\.commandcode\\config.json.tmp-<ts>-<rand>",
"write_mode": 384,
"config_hash_before": "<sha256(JSON.stringify(config))>",
"started_at": "<ISO timestamp>"
}
Before rename, log:
{
"type": "config_write_before_rename",
"trace_id": "<...>",
"temp_path": "...",
"temp_size_bytes": <number>,
"destination_exists": <bool>,
"elapsed_ms": <number>
}
On rename failure, log:
{
"type": "config_write_rename_failed",
"trace_id": "<...>",
"node_error_code": "EPERM",
"errno": -4040,
"native_hresult": "<Windows HRESULT, e.g. 0x80070020>",
"retry_count": 0,
"temp_cleanup_result": "<ok|failed>",
"config_hash_after": null,
"failed_at": "<ISO timestamp>"
}
At web-tool start/end:
{
"type": "web_tool_trace",
"tool": "web_search|web_fetch",
"call_id": "<id>",
"trace_id": "<getTraceId()>",
"concurrency_group": "<turn id>",
"config_hash_before": "<...>",
"config_hash_after": "<...>",
"saveUserConfig_triggered": <bool>
}
Required regression tests
| # | Test | Inputs | Expected behavior | Pass/fail signal |
|---|---|---|---|---|
| 1 | Parallel saveUserConfig() calls to one destination |
N concurrent writers, same config.json |
All complete; final file valid JSON = last writer's content | file parses; content == last config; zero EPERM |
| 2 | Rename EPERM/sharing-violation retry | Rename throws EPERM on first attempt, succeeds on retry | Bounded retry (3 attempts, 50/100/200ms) recovers | log shows retry_count: 3 then ok: true; no thrown error |
| 3 | Web calls without config mutation do not write | Concurrent web_search/web_fetch only | No saveUserConfig invocation |
zero config_write_attempt log entries |
| 4 | Two Command Code processes, one config path | Two cmdc PIDs |
Inter-process lock serializes; both succeed | both write log entries have monotonic started_at; no EPERM |
| 5 | Temporary cleanup after failed rename | Rename fails after retries exhausted | .tmp-* removed; original config.json untouched |
no .tmp-* in dir; file unchanged |
| 6 | Config validity after concurrent failures | Concurrent writes including forced failures | config.json remains valid JSON |
cmdc config list --json succeeds; JSON parses |
| 7 | Web error classifies config-write race | Force rename EPERM during a web call | Error states config_write race, not generic web failure |
error message contains config_write + trace_id + retry_count |
| 8 | Serial and parallel web calls produce equivalent results | Same N queries, serial vs parallel | Identical result sets (modulo ranking nondeterminism) | result count + top-3 URLs match |
| 9 | Scheduler concurrency safety includes adjacent lifecycle writes | Concurrent web + preference/compact/model writes | All lifecycle writers serialize config access | no duplicate rename to same path within 100ms |
| 10 | Error includes trace_id | Any config-write failure | User-visible error contains trace_id |
error string matches /trace_id=[a-f0-9-]+/ |
Requested fix
- Serialize all writes to
config.jsonwith a process-wide write queue/mutex (the config writer must be single-writer per process). - Add an inter-process lock (or advisory lock on the temp-write) if multiple
cmdcprocesses can run concurrently. - Skip
saveUserConfig()when read-only web calls produce no state change (move preference/model/compact persistence out of the web tool critical path, or no-op on identical content). - Add bounded retry/backoff (3 attempts, 50/100/200ms) for Windows
EPERMand sharing violations onrename. - Make the error path classify EPERM-as-race distinctly from ACL-denial (do not lose it in
categorizeError'suo.PERMISSIONbucket; emit aconfig_write_racecategory). - Attach
trace_id,session_id,pid, and caller to config-write failures so the boundary is recoverable from the error message. - After a config-write retry succeeds, re-attempt the original web call (the web tool should not hard-fail because of a transient config race).
- Add the Windows concurrency regression tests above.
- Correct
cmdc infoshell detection separately (reportscmd.exeunder PowerShell/WT) — tracked as a distinct issue.
Workaround (current)
Run independent web calls serially. Restarting Command Code may reduce recurrence temporarily. No permission-mode change is required or should be treated as the fix.
Related issues checked
- #603: macOS 26
EPERMcaused by shared giget cache provenance. Different platform, path, subsystem, reproduction. - #615: Windows command-name documentation typo. Unrelated.
- #686: original report of the same
config.jsonrename EPERM. This issue is the intensified diagnostic companion with inlined evidence and regression matrix.
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Evaluación
Este issue todavía no se ha evaluado.