Summary
scrubSecrets() throws result.replace is not a function when message is not a string at runtime. This surfaces to OpenCode as a recurring "failed to load plugin" error, which disables the morph_edit tool for the affected session and spams the OpenCode log.
Symptom (from OpenCode host log)
Observed repeatedly across many sessions/days:
level=ERROR message="failed to load plugin"
path=file:///.../node_modules/opencode-morph-fast-apply/index.ts
error="result.replace is not a function.
(In 'result.replace(/Bearer\s+[A-Za-z0-9_\-\.]{10,}/gi, \"Bearer ***REDACTED***\")',
'result.replace' is undefined)"
The unique regex pins the throw site to index.ts:
// index.ts (v1.10.0)
export function scrubSecrets(message: string, apiKey?: string): string {
let result = message;
if (apiKey) {
result = result.split(apiKey).join("***REDACTED***"); // also throws on non-string
}
result = result.replace( // <-- throws here
/Bearer\s+[A-Za-z0-9_\-\.]{10,}/gi,
"Bearer ***REDACTED***",
);
return result;
}
Root cause
scrubSecrets is typed (message: string, ...) but is not defensive at runtime. When a caller passes a non-string (e.g. an Error object, undefined, or a number from an error/exception path), result.replace / result.split are undefined and throw. The TS type does not protect against this because the offending value originates from a catch/error path that escapes static typing.
In-repo callers:
-
index.ts:124 — scrubSecrets(\Morph API error (${response.status}): ${errorText}`, apiKey)`
-
index.ts:184 — scrubSecrets(\Morph API request failed: ${error.message}`, apiKey)`
-
src/execute.ts:175 — scrubSecrets(result.error || "unknown error", MORPH_API_KEY)
These pass strings in the happy path, but the runtime error proves at least one path delivers a non-string message (or a non-string apiKey reaching split). The function should never throw while scrubbing an error message — failing to scrub turns a recoverable error into a plugin-load failure.
Proposed fix
Harden scrubSecrets to coerce input and never throw:
export function scrubSecrets(message: unknown, apiKey?: unknown): string {
let result =
typeof message === "string"
? message
: message instanceof Error
? message.message
: String(message ?? "");
if (typeof apiKey === "string" && apiKey.length > 0) {
result = result.split(apiKey).join("***REDACTED***");
}
result = result.replace(
/Bearer\s+[A-Za-z0-9_\-\.]{10,}/gi,
"Bearer ***REDACTED***",
);
return result;
}
Optionally also audit callers to pass err instanceof Error ? err.message : String(err) rather than raw error objects.
A regression test should cover: Error instance, undefined, null, number, and a string containing a Bearer token + the API key.
Impact
- Severity: medium. No data loss; the plugin simply fails to load, so
morph_edit is unavailable and agents fall back to native edit tools. Log noise on every (re)load.
- Not a crash cause: filing this separately from an unrelated OpenCode/Bun host crash investigated at the same time — this defect does not segfault the host, it only fails plugin load.
Environment
opencode-morph-fast-apply v1.10.0 (installed)
- OpenCode 1.17.11
- Runtime: Bun (OpenCode host)
- OS: Linux (WSL2,
6.6.114.1-microsoft-standard-WSL2)
If this is already fixed on main/a newer published version, please disregard — installed copy is 1.10.0.
Summary
scrubSecrets()throwsresult.replace is not a functionwhenmessageis not a string at runtime. This surfaces to OpenCode as a recurring "failed to load plugin" error, which disables themorph_edittool for the affected session and spams the OpenCode log.Symptom (from OpenCode host log)
Observed repeatedly across many sessions/days:
The unique regex pins the throw site to
index.ts:Root cause
scrubSecretsis typed(message: string, ...)but is not defensive at runtime. When a caller passes a non-string (e.g. anErrorobject,undefined, or a number from an error/exception path),result.replace/result.splitare undefined and throw. The TS type does not protect against this because the offending value originates from acatch/error path that escapes static typing.In-repo callers:
index.ts:124—scrubSecrets(\Morph API error (${response.status}): ${errorText}`, apiKey)`index.ts:184—scrubSecrets(\Morph API request failed: ${error.message}`, apiKey)`src/execute.ts:175—scrubSecrets(result.error || "unknown error", MORPH_API_KEY)These pass strings in the happy path, but the runtime error proves at least one path delivers a non-string
message(or a non-stringapiKeyreachingsplit). The function should never throw while scrubbing an error message — failing to scrub turns a recoverable error into a plugin-load failure.Proposed fix
Harden
scrubSecretsto coerce input and never throw:Optionally also audit callers to pass
err instanceof Error ? err.message : String(err)rather than raw error objects.A regression test should cover:
Errorinstance,undefined,null, number, and a string containing aBearertoken + the API key.Impact
morph_editis unavailable and agents fall back to native edit tools. Log noise on every (re)load.Environment
opencode-morph-fast-applyv1.10.0 (installed)6.6.114.1-microsoft-standard-WSL2)If this is already fixed on
main/a newer published version, please disregard — installed copy is 1.10.0.