Summary
scanFailureMessage classifies every scan failure with classifyConnectionFailure, which matches bare words such as network, connection, permission denied, 401 and 403 anywhere in the message — including inside repository paths, Git refs and filesystem error text supplied by the user. When the classification is anything other than unknown, the original message is discarded and replaced with connectivity or authorization advice that does not apply.
Classification is applied to the whole failure:
|
function scanFailureMessage( |
|
error: unknown, |
|
authentication: ScanAuthentication | null, |
|
): string { |
|
switch (classifyConnectionFailure(error)) { |
|
case "unauthorized": |
|
return authentication?.method === "api_key" |
|
? `Authentication failed using ${authentication.source}. ` + |
|
"Your ChatGPT sign-in was not used. " + |
|
"Retry with '--auth chatgpt' or provide a valid API key." |
|
: "Authentication failed using stored ChatGPT credentials. " + |
|
"Sign in again with 'codex-security login' or provide a valid API key."; |
|
case "forbidden": |
|
return authentication?.method === "api_key" |
|
? `The API key from ${authentication.source} cannot access the configured model. ` + |
|
"Retry with '--auth chatgpt' or use an API key with model access." |
|
: "The stored ChatGPT credentials cannot access the configured model. " + |
|
"Use an account or API key with model access."; |
|
case "rate_limited": |
|
return "The configured account reached its rate limit. Wait and retry."; |
|
case "network_error": |
|
return "The model service could not be reached. Check your network connection and try again."; |
|
case "timeout": |
|
return "The connection timed out. Check your network connection and try again."; |
|
case "unknown": |
|
return cliErrorMessage(error); |
|
} |
|
} |
and the patterns are plain substring matches:
|
export function classifyConnectionFailure( |
|
error: unknown, |
|
): |
|
| "rate_limited" |
|
| "unauthorized" |
|
| "forbidden" |
|
| "network_error" |
|
| "timeout" |
|
| "unknown" { |
|
const message = error instanceof Error ? error.message : String(error); |
|
if (/\b(?:sqlite3?|database|workbench)\b/iu.test(message)) { |
|
return "unknown"; |
|
} |
|
if ( |
|
/\brate[_ -]?limit(?:ed|[_ -]exceeded)?\b|\b429\b|\btoo many requests\b/iu.test( |
|
message, |
|
) |
|
) { |
|
return "rate_limited"; |
|
} |
|
if ( |
|
/\b401\b|\bunauthori[sz]ed\b|\binvalid[_ -](?:api[_ -]?key|authentication|token|credentials?)\b|\b(?:expired|revoked)[_ -](?:api[_ -]?key|token|credentials?)\b|\b(?:api[_ -]?key|token|credentials?)(?: has)? (?:expired|been revoked)\b/iu.test( |
|
message, |
|
) |
|
) { |
|
return "unauthorized"; |
|
} |
|
if ( |
|
/\b403\b|\bforbidden\b|\bpermission denied\b|\b(?:model|organization|project) access\b|\b(?:access denied|do not have access|not authorized|insufficient permissions)\b|\bmodel[_ -]?not[_ -]?found\b/iu.test( |
|
message, |
|
) |
|
) { |
|
return "forbidden"; |
|
} |
|
if ( |
|
/\b(?:ENOTFOUND|ECONNRESET|ECONNREFUSED|EHOSTUNREACH|ETIMEDOUT)\b|\b(?:network|connection|TLS|DNS)\b|\berror sending request\b/iu.test( |
|
message, |
|
) |
|
) { |
|
return "network_error"; |
|
} |
|
if (/\b(?:timed? out|timeout)\b/iu.test(message)) return "timeout"; |
|
return "unknown"; |
Only the unknown branch calls cliErrorMessage(error); every other branch returns fixed advice, so the real cause reaches neither stderr nor the JSON error field.
The false-positive class is already known — classifyConnectionFailure short-circuits on sqlite/database/workbench text and tests-ts/api.test.ts pins that guard — but the guard covers only workbench and database wording, not user-supplied paths, refs, or filesystem errors.
Affected version and environment
- Released package:
@openai/codex-security@0.1.1
- Confirmed on current
main at f22d4a36f26d16287bcdfd707b369116e02a08c3
- macOS 26.5.2 (build 25F84)
- Node.js v24.5.0
- Bun 1.3.14
Steps to reproduce
Driving the real main() with the existing tests-ts/cli-fixtures.ts harness, where the injected run() rejects with the error the SDK would genuinely raise:
const err = capture();
const exitCode = await main(
["scan", ".", "--path", "src/network/client.ts"],
out.stream,
err.stream,
dependencies({
onRun: () => {
throw new InvalidTargetError("Path target does not exist: src/network/client.ts");
},
}),
);
Observed results for three realistic failures:
### missing --path containing the word 'network'
thrown by run(): Path target does not exist: src/network/client.ts
shown to user: codex-security: The model service could not be reached. Check your network connection and try again.
exit code: 2
### unknown git ref containing the word 'connection'
thrown by run(): unknown Git ref: origin/connection-fix
shown to user: codex-security: The model service could not be reached. Check your network connection and try again.
exit code: 2
### read-only TMPDIR (EACCES from mkdtemp)
thrown by run(): EACCES: permission denied, mkdtemp '/tmp/openai-codex-security-home-XXXXXX'
shown to user: codex-security: The stored ChatGPT credentials cannot access the configured model. Use an account or API key with model access.
exit code: 2
The exact messages come from InvalidTargetError in src/targets.ts (Path target does not exist: and unknown Git ref:) and from createIsolatedHome's mkdtemp in src/runtime.ts.
Expected behavior
Connectivity and authorization advice should be produced only for failures that actually originate from the model transport. Local input validation errors and filesystem errors should surface their own message so the user can act on the real cause.
Actual behavior
A mistyped path, a missing Git ref, or an unwritable temporary directory is reported as a network outage or a credential problem, and the actual error text is dropped entirely.
Impact
Beyond the immediate confusion, this actively hides the cause during diagnosis. A user following the printed advice will check their network or reconfigure credentials while the real problem is a typo in --path.
This may also be worth considering in relation to #26, where a scan failed with "The model service could not be reached" while codex exec worked from the same shell. Whatever the underlying cause there was, this masking is what prevented the real error from being visible in the report.
The same classifier also gates retry-versus-fatal handling in the event stream (src/api.ts, the error event branch), so a stream error whose text merely contains "permission denied" is treated as an unrecoverable authorization failure rather than a retryable one.
Suggested direction
Apply classifyConnectionFailure only to errors known to come from the Codex transport, and let local validation and filesystem errors fall through to cliErrorMessage. Alternatively, always include the original message alongside the advice so no failure is reported without its cause.
Summary
scanFailureMessageclassifies every scan failure withclassifyConnectionFailure, which matches bare words such asnetwork,connection,permission denied,401and403anywhere in the message — including inside repository paths, Git refs and filesystem error text supplied by the user. When the classification is anything other thanunknown, the original message is discarded and replaced with connectivity or authorization advice that does not apply.Classification is applied to the whole failure:
codex-security/sdk/typescript/src/cli.ts
Lines 2478 to 2505 in f22d4a3
and the patterns are plain substring matches:
codex-security/sdk/typescript/src/api.ts
Lines 1543 to 1585 in f22d4a3
Only the
unknownbranch callscliErrorMessage(error); every other branch returns fixed advice, so the real cause reaches neither stderr nor the JSONerrorfield.The false-positive class is already known —
classifyConnectionFailureshort-circuits onsqlite/database/workbenchtext andtests-ts/api.test.tspins that guard — but the guard covers only workbench and database wording, not user-supplied paths, refs, or filesystem errors.Affected version and environment
@openai/codex-security@0.1.1mainatf22d4a36f26d16287bcdfd707b369116e02a08c3Steps to reproduce
Driving the real
main()with the existingtests-ts/cli-fixtures.tsharness, where the injectedrun()rejects with the error the SDK would genuinely raise:Observed results for three realistic failures:
The exact messages come from
InvalidTargetErrorinsrc/targets.ts(Path target does not exist:andunknown Git ref:) and fromcreateIsolatedHome'smkdtempinsrc/runtime.ts.Expected behavior
Connectivity and authorization advice should be produced only for failures that actually originate from the model transport. Local input validation errors and filesystem errors should surface their own message so the user can act on the real cause.
Actual behavior
A mistyped path, a missing Git ref, or an unwritable temporary directory is reported as a network outage or a credential problem, and the actual error text is dropped entirely.
Impact
Beyond the immediate confusion, this actively hides the cause during diagnosis. A user following the printed advice will check their network or reconfigure credentials while the real problem is a typo in
--path.This may also be worth considering in relation to #26, where a scan failed with "The model service could not be reached" while
codex execworked from the same shell. Whatever the underlying cause there was, this masking is what prevented the real error from being visible in the report.The same classifier also gates retry-versus-fatal handling in the event stream (
src/api.ts, theerrorevent branch), so a stream error whose text merely contains "permission denied" is treated as an unrecoverable authorization failure rather than a retryable one.Suggested direction
Apply
classifyConnectionFailureonly to errors known to come from the Codex transport, and let local validation and filesystem errors fall through tocliErrorMessage. Alternatively, always include the original message alongside the advice so no failure is reported without its cause.