Skip to content

scrubSecrets() throws "result.replace is not a function" on non-string input → recurring plugin-load failure #6

Description

@JRedeker

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:124scrubSecrets(\Morph API error (${response.status}): ${errorText}`, apiKey)`
  • index.ts:184scrubSecrets(\Morph API request failed: ${error.message}`, apiKey)`
  • src/execute.ts:175scrubSecrets(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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions