Summary
On Windows, gjc dies with an uncaught EPERM before producing any output whenever its working directory is not writable. The most common way to hit this is a console launched without a "Start in" directory, which defaults to C:\Windows\system32.
[Uncaught Exception] Error: EPERM: operation not permitted, mkdir 'C:\Windows\system32\.gjc'
at mkdirSync (unknown)
at flushState (.../modes/shared/agent-wire/workflow-gate-broker.ts:1044:3)
at commit (.../workflow-gate-broker.ts:1080:8)
at beginRuntimeInstance (.../workflow-gate-broker.ts:1128:8)
at new WorkflowGateBroker (.../workflow-gate-broker.ts:1237:14)
at new BrokerWorkflowGateEmitter (.../workflow-gate-broker.ts:159:22)
at #constructWorkflowGateEmitter (.../session/agent-session.ts:8080:14)
at #bindWorkflowGateEmitter (.../session/agent-session.ts:8110:41)
at new AgentSession (.../session/agent-session.ts:2870:8)
at createAgentSession (.../sdk/session.ts:3000:17)
{"code":"EPERM","path":"C:\\Windows\\system32\\.gjc","syscall":"mkdir","errno":-1}
Reproduction
Any non-elevated shell, with a cwd the user cannot write to:
cd C:\Windows\System32
gjc -p "say hi"
100% reproducible on 0.13.2. The session never starts, and the error arrives as a raw ErrnoException rather than anything actionable.
Root cause
FileGateStore's constructor already documents the invariant this violates:
// Load eagerly so a corrupt store fails closed at construction, but defer
// directory creation to the first write so constructing a store under a
// non-writable cwd (e.g. a session that never emits a gate) never throws.
That deferral (added in #2078) is defeated by WorkflowGateBroker's constructor, which writes immediately:
constructor(runId, store, hooks = {}, instanceId = crypto.randomUUID()) {
this.store.beginRuntimeInstance(this.instanceId);
}
On a fresh store state.runtimeInstanceId is undefined, so beginRuntimeInstance always falls through to commit(next) → flushState → mkdirSync. "The first write" is therefore always construction, and the store's gate-state path is cwd-derived (sessionStateDir(getCwd(), sessionId) at agent-session.ts:8077). So every session start mkdirs under cwd, and an unwritable cwd is fatal.
Two things compound it:
mkdirSync is the one statement in flushState sitting outside the try/catch that turns every other write failure into a typed GateStoreWriteError, so this failure escapes the store abstraction entirely.
- Nothing is actually being persisted at that moment. A store with no gates and no counters has nothing a later process could recover or quarantine — the write exists only to stamp a runtime instance id.
Suggested fix
Restore the documented invariant: skip the flush in beginRuntimeInstance when the state holds no gates and no counters, letting the instance id ride along with the first real mutation, and move mkdirSync inside the existing try so any genuine failure surfaces as GateStoreWriteError.
I have this working with regression tests and will open a PR shortly.
Note on a second, non-fatal path
With the above fixed, gjc -p completes successfully from system32 (exit 0). A telemetry callback still attempts the same mkdir, but that path already catches and swallows it:
[pi-agent] onChatUsage threw; swallowing telemetry callback failure EPERM: operation not permitted, mkdir 'C:\Windows\System32\.gjc'
Non-blocking, and out of scope for this issue — flagging it in case it is worth the same lazy treatment.
Environment
@gajae-code/coding-agent 0.13.2 (bun global install)
- Bun 1.3.14
- Windows 11 Pro 26200
Summary
On Windows,
gjcdies with an uncaughtEPERMbefore producing any output whenever its working directory is not writable. The most common way to hit this is a console launched without a "Start in" directory, which defaults toC:\Windows\system32.Reproduction
Any non-elevated shell, with a cwd the user cannot write to:
100% reproducible on
0.13.2. The session never starts, and the error arrives as a rawErrnoExceptionrather than anything actionable.Root cause
FileGateStore's constructor already documents the invariant this violates:That deferral (added in #2078) is defeated by
WorkflowGateBroker's constructor, which writes immediately:On a fresh store
state.runtimeInstanceIdisundefined, sobeginRuntimeInstancealways falls through tocommit(next)→flushState→mkdirSync. "The first write" is therefore always construction, and the store's gate-state path is cwd-derived (sessionStateDir(getCwd(), sessionId)atagent-session.ts:8077). So every session start mkdirs under cwd, and an unwritable cwd is fatal.Two things compound it:
mkdirSyncis the one statement influshStatesitting outside the try/catch that turns every other write failure into a typedGateStoreWriteError, so this failure escapes the store abstraction entirely.Suggested fix
Restore the documented invariant: skip the flush in
beginRuntimeInstancewhen the state holds no gates and no counters, letting the instance id ride along with the first real mutation, and movemkdirSyncinside the existing try so any genuine failure surfaces asGateStoreWriteError.I have this working with regression tests and will open a PR shortly.
Note on a second, non-fatal path
With the above fixed,
gjc -pcompletes successfully fromsystem32(exit 0). A telemetry callback still attempts the same mkdir, but that path already catches and swallows it:Non-blocking, and out of scope for this issue — flagging it in case it is worth the same lazy treatment.
Environment
@gajae-code/coding-agent0.13.2 (bun global install)