GravireiGitHubGravireiClaude Opus 4.6coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>openhands
aa936cda11 Centralize credential redaction in src/utils/redaction.ts + channel gate tests (#1711)
* feat(utils): add centralized redaction utility

Single source of truth for stripping API keys, tokens, and other
secrets from strings and JSON. Provider env-var coverage is generated
from getKnownProviderSecretEnvKeys() so adding a new provider cannot
silently create an unredacted path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(Feedback): import redactSensitiveInfo from utils

Remove the inline 40-line regex implementation in favor of the
centralized redaction utility, eliminating drift between Feedback
and the transcript share path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(submitTranscriptShare): import redactSensitiveInfo from utils

Update import path to point at the centralized utility instead of the
Feedback component. Removes the implicit re-export contract that
required Feedback.tsx to keep redactSensitiveInfo exported.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(log,debug): redact secrets in default error and debug output

Wire the centralized redaction utility into logError and logForDebugging
so secrets cannot leak into in-memory error logs or the debug file even
if a caller forgets to pass through redactSensitiveInfo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(api/logging): redact error message in logAPIError

Apply the centralized redaction utility to the error string passed to
logEvent so analytics events cannot capture unredacted credentials from
upstream API failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve merge conflict from upstream sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(channelNotification): allow null in getEffectiveChannelAllowlist signature

ChannelsNotice.tsx passes getSubscriptionType() which returns
SubscriptionType | null, but the signature only accepted string |
undefined. Widen to string | null so the call site typechecks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(redaction): exclude specific token fields from redaction process

* fix(redaction): lower AIza minimum length to {10,}

Real GCP/Gemini keys are 39 chars total (4 prefix + 35 suffix), but
the {35} suffix bound missed short tokens like 'AIzaSyDUMMY-secret-token'
(21 chars after AIza). Lower to {10,} to match the diagnostics module
and catch any AIza-shaped value. Same precision trade-off the
diagnostics redaction makes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction,log): address review feedback

- Drop quotes from ANTHROPIC/OPENAI key negative lookarounds so
  JSON-shaped values like "sk-ant-..." redact.
- Add private_key pattern to GENERIC_HEADER_FIELD_PATTERN and
  privatekey to SENSITIVE_FIELD_SUBSTRINGS.
- logError now builds a sanitized Error (redacted message + stack)
  before passing to the sink and queue, not just the in-memory log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(redaction): consolidate into single module + add channel gate tests

Address the three P2 review findings on the central-redaction PR:

[1] Consolidate four redaction modules into src/utils/redaction.ts.
    Previously lived in:
      - src/utils/redaction.ts            (logs/bug reports/transcript shares)
      - src/utils/urlRedaction.ts         (URL display)
      - src/utils/statusRedaction.ts      (/status output)
      - src/utils/diagnostics/redaction.ts (doctor reports)
    The four surfaces share the same regex set / credential lists
    but had drifted into separate per-domain files. Merged into
    one module; deleted the three shim files. Updated six direct
    consumers (openaiShim.ts, ProviderManager.tsx, status.tsx,
    requestSizeBreakdown.ts, diagnostics/issueReport.ts,
    scripts/system-check.ts) and three test files to import from
    redaction.js.

[2] Add gateChannelServer() test coverage.
    src/services/mcp/channelNotification.test.ts: 13 cases for the
    six gate paths (capability, runtime, session, marketplace,
    plugin allowlist, server-entry dev) plus end-to-end register.
    Mocks channelAllowlist.js (GrowthBook-backed) so tests stay
    independent of feature-flag state.

[3] Apply jsonRedactor in transcript share.
    src/components/FeedbackSurvey/submitTranscriptShare.ts now does
    redactSensitiveInfo(jsonStringify(data, jsonRedactor)) — the
    key-aware redaction applies during serialization, and the text
    pass stays as defense in depth for free-form fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(channelNotification): cover findChannelEntry multi-candidate branch

Regression test for the disambiguation path in `findChannelEntry`
(channelNotification.ts:201-230): when two same-name plugin entries
exist in the allowed-channels list with different marketplaces,
`pluginSource` must select the matching entry before the marketplace
and allowlist gates evaluate.

Without this branch being exercised, the gate could lock onto
whichever entry sorts first and either skip the user's real
installation or wrongly authorize a typo-squatted one.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction): align URL fallback regex + add path-prefix boundary check

Two related redaction correctness fixes:

[1] URL fallback regex covers the same parameter set as the primary
    path. The malformed-URL branch in `redactUrlForDisplay` previously
    had a hand-rolled alternation of credential parameter names that
    could drift behind `SENSITIVE_URL_QUERY_PARAM_TOKENS`. New
    `MALFORMED_URL_PARAM_PATTERN` derives from that same list, so
    the two paths can never diverge. Tests cover the full credential
    set (`api_key`, `access_token`, `refresh_token`, `signature`,
    `sig`, `secret`, `password`, `apikey`) plus a non-sensitive
    `model` that must survive.

[2] `redactPathForStatus` now requires a path-separator boundary
    after the home prefix. The previous `startsWith` check matched
    `/home/alice2/project` against `/home/alice` and emitted
    `~2/project`. The fix requires the character at
    `normalizedCandidate.length` to be `/` or `\` so `alice` no longer
    matches `alice2` or `alice.bak`. Test pins the false-positive
    paths and the true-positive (`/home/alice/project` → `~/project`).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(channel,redaction): restore dev-channel warning + align URL fallback

Two related security fixes:

[1] Restore DevChannelsDialog when --dangerously-load-development-channels
    is passed and the channels feature is enabled. The previous logic
    skipped the dialog when OAuth was absent, which was safe only while
    gateChannelServer() blocked no-OAuth sessions. With the OAuth/org-
    policy gates removed in this PR, an API-key session could pass the
    flag, skip the warning, and still register the dev channel. The
    only remaining skip is the genuinely-disabled feature case
    (`!isChannelsEnabled()`), where the dialog is moot.

[2] Malformed-URL fallback now uses the same substring predicate as
    the primary `URL` parser path. The previous regex matched only
    exact parameter names (`api_key=`, `access_token=`, …), so
    `my_api_key=SECRET` and `x_access_token=TOKEN` slipped through
    unchanged even though `shouldRedactUrlQueryParam` flags them as
    sensitive. New `redactMalformedQuery` walks the query pairs and
    runs the predicate on each key. Three new tests cover prefixed
    keys, non-sensitive keys, and fragment preservation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction): widen key boundary class + tighten dev-channel comment

Two small follow-ups from the latest CodeRabbit review:

[1] Boundary class on key-prefix patterns widened from `[A-Za-z0-9]`
    to `[A-Za-z0-9_-]` so a raw key embedded in a JSON string value
    (`"sk-ant-..."`, `"AIza..."`, `"ghp_..."`, etc.) is still caught.
    Quotes act as delimiters, not blockers — the previous boundary
    class was correct for unquoted text but let quoted keys slip
    through.

[2] Tighten the dev-channel dialog comment in interactiveHelpers.tsx
    so future readers don't misread the security boundary. Skip
    condition is `isChannelsEnabled()` (the channels feature flag
    gate), not KAIROS / KAIROS_CHANNELS as the previous wording
    implied. Comment now matches the code.

Skipped with reason:
- getEffectiveChannelAllowlist divergence from gateChannelServer
  allowlist — by design; the effective-list override is a UI hint
  consumed only by ChannelsNotice for the org-override indicator.
  Trust boundary is enforced by gateChannelServer() reading the
  hardcoded ledger.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(redaction,channel): address P1/P2 review findings

P1 - malformed URL fallback secrets:
- Decode percent-encoded query param keys via decodeURIComponent() before
  applying shouldRedactUrlQueryParam (e.g. %74oken -> token)
- Stop userinfo regex at ? and # delimiters to avoid consuming query params
  when matching @ signs in email addresses or fragment delimiters

P2 - channel notice/gate allowlist sync:
- Remove org override path from getEffectiveChannelAllowlist() so
  ChannelsNotice startup guidance uses the same ledger source as
  gateChannelServer's runtime enforcement
- Simplify ChannelsNotice to drop unused sub/policy params and the
  source === 'org' conditional

* fix(channel): apply marketplace matching to permission relays, remove stale OAuth/org-policy blockers, add dev-channel dialog coverage

P1: Thread runtime pluginSource through filterPermissionRelayClients
so findChannelEntry disambiguates same-name plugin entries from
different marketplaces before sending permission request previews.

P2: Remove stale noAuth and policyBlocked branches from ChannelsNotice
that would render '--channels ignored' before reaching the listening
message, confusing non-OAuth users.

P2: Add test coverage that mocks isChannelsEnabled() both true and
false, verifies DevChannelsDialog appears with onAccept marking entries
dev:true in the enabled case, and verifies the disabled branch registers
entries directly without dialog.

* test(dev-channel): clarify count assertion comment + add afterEach with mock.restore()

* fix(channel): mirror marketplace gate in permission relay + restore mock

Two follow-ups from the latest review:

[1] Permission relay predicate no longer relies on findChannelEntry
    alone. After resolving the entry, the predicate now requires a
    runtime pluginSource whose marketplace matches the session
    entry's marketplace for plugin-kind entries — mirroring the
    gateChannelServer check at channelNotification.ts:303-312. A
    `plugin:slack@evilcorp` client whose session allows
    `plugin:slack@anthropic` is now rejected instead of piggy-backing
    on the approved entry to receive permission-request previews.
    Server-kind entries still match on bare name.

[2] bugfixes.test.ts now re-registers the real channelAllowlist
    module in afterEach via a cache-busted reference, so the
    neighbor channelNotification.test.ts continues to import
    getChannelAllowlist after this suite runs. mock.restore() does
    not clear module-level mock.module() overrides in bun (the
    registry is process-global). Pattern matches compact.test.ts:27-36.

Also expanded the dev-map count comment in bugfixes.test.ts to
document the security invariant (a dev entry must never be confused
with a production entry in the allowlist check) per CodeRabbit's
request.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(redaction): consolidate into single module + add channel gate tests

Address the three P2 review findings on the central-redaction PR:

[1] Consolidate four redaction modules into src/utils/redaction.ts.
    Previously lived in:
      - src/utils/redaction.ts            (logs/bug reports/transcript shares)
      - src/utils/urlRedaction.ts         (URL display)
      - src/utils/statusRedaction.ts      (/status output)
      - src/utils/diagnostics/redaction.ts (doctor reports)
    The four surfaces share the same regex set / credential lists
    but had drifted into separate per-domain files. Merged into
    one module; deleted the three shim files. Updated six direct
    consumers (openaiShim.ts, ProviderManager.tsx, status.tsx,
    requestSizeBreakdown.ts, diagnostics/issueReport.ts,
    scripts/system-check.ts) and three test files to import from
    redaction.js.

[2] Add gateChannelServer() test coverage.
    src/services/mcp/channelNotification.test.ts: 13 cases for the
    six gate paths (capability, runtime, session, marketplace,
    plugin allowlist, server-entry dev) plus end-to-end register.
    Mocks channelAllowlist.js (GrowthBook-backed) so tests stay
    independent of feature-flag state.

[3] Apply jsonRedactor in transcript share.
    src/components/FeedbackSurvey/submitTranscriptShare.ts now does
    redactSensitiveInfo(jsonStringify(data, jsonRedactor)) — the
    key-aware redaction applies during serialization, and the text
    pass stays as defense in depth for free-form fields.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): align malformed URL fragment expectation with preservation behavior

* fix: address review findings P1 and P2

[P1] Enforce dev flag for server-kind entries in permission relay
predicate, matching gateChannelServer() behavior. Add coverage for
both dev and non-dev server relay paths.

[P2] Drop fragments in malformed URL fallback (redactMalformedQuery)
to match the valid-URL path, preventing credential leaks via
fragment-carried tokens. Update existing tests and add regression
for fragment-only malformed URLs.

* test(relay): add plugin-kind marketplace regression tests

* fix: address review findings P1 and P2

[P1] Add PEM private key redaction pattern to redactSensitiveInfo
so multi-line PEM values are fully consumed instead of leaking
after the first whitespace. Add [ to generic header pattern's
value exclusion set to prevent re-consuming [REDACTED] tokens.

[P2] Use truthy check (Boolean()) for claude/channel capability in
filterPermissionRelayClients to match gateChannelServer's behavior,
rejecting explicit false capabilities.

* fix(debug): redact before JSON-stringify multiline messages

Reorder logForDebugging so redactSensitiveInfo runs before jsonStringify,
ensuring PEM/private-key patterns match the raw (unescaped) message text
rather than the JSON-encoded form where colons and quotes are escaped.

* test(debug): add end-to-end regression for multiline PEM redaction in logForDebugging

Uses mock.module on process.js to capture stderr output and exercises the
full logForDebugging path with multiline PEM private_key input, verifying
the redact-before-JSON-stringify ordering produces redacted output.

* fix(test): preserve original process.env.DEBUG and process.argv in logForDebugging test hooks

* fix: address PR review findings P1-P3/P5-P7

- P1: clear isDebugMode/isDebugToStdErr memoize caches in test beforeEach
      + cache-busting query param for fresh debug.ts imports
- P2: restore mock.module afterAll instead of leaking mock
      + mutate err in-place in logError to preserve name/cause
- P3: post-processing regex absorbs trailing bracket content after [REDACTED]
- P5: (was P3) expand jsonRedactor EXCLUDED_KEYS for maxTokens etc.
- P7: capture HOME/USERPROFILE per-test instead of at module scope

* fix: address CodeRabbit review findings

- interactiveHelpers.tsx: update dev-channel comment — OAuth/org-policy
  gates removed from gateChannelServer(), org policy is not enforced
- channelNotification.test.ts: add afterAll mock.restore() to clean up
  process-global channelAllowlist.js mock
- channelNotification.ts: fix comments — isChannelsEnabled() still reads
  tengu_harbor, not always true
- log.ts: sanitize err.message and err.stack separately so message
  doesn't get replaced with full stack trace
- redaction.ts: add 'i' flag to redactHomePath regex for Windows
  case-insensitive path matching

* fix: address second review round

- interactiveHandler.ts: [P2] redact input_preview via redactSensitiveInfo
  before sending to channel servers
- log.ts: [P3] copy error via Object.assign(Object.create(err), err)
  before sanitizing instead of mutating in-place

* fix: address CodeRabbit second round

- channelPermissions.ts: redact before truncate in truncateForPreview
  so partial credentials don't leak at the 200-char boundary
- interactiveHandler.ts: remove outer redactSensitiveInfo — now
  handled inside truncateForPreview
- log.ts: derive errorInfo.error from already-sanitized sanitizedErr;
  fix Object.assign comment to accurately describe what is copies

* fix: improve permission relay client filtering and enhance redaction functions

* fix: address third review round (P1, P2, P3)

- P1: update test expectations for [REDACTED_*] output format
- P2: add total_tokens, prompt_tokens, completion_tokens to jsonRedactor EXCLUDED_KEYS
- P3: remove ) and } from GENERIC_HEADER_FIELD_PATTERN value capture to prevent content leak after embedded parens
- Fix buildKnownEnvVarPattern capture group to preserve env-var separator ([REDACTED])
- Add & to GENERIC_CREDENTIAL_ENV_PATTERN value exclusion to prevent URL query over-consumption

* fix: address latest reviewer P2/P3 findings (errorLogSink redaction, X_API_KEY/AUTHORIZATION patterns, regression tests)

* fix: address reviewer P1/P2 — bracketed values and multi-word header values

- P1: Remove  and  from value captures in X_API_KEY_PATTERN,
  AUTHORIZATION_PATTERN, GENERIC_HEADER_FIELD_PATTERN,
  GENERIC_CREDENTIAL_ENV_PATTERN so bracketed secrets like
  are fully redacted instead of passing through unchanged.

- P2: Widen header-style value captures to include spaces by removing
   from exclusions, using  as delimiter (stops at newlines
  and URL query separators). Fixes multi-word leaks:
  , ,
  , .

- GENERIC_CREDENTIAL_ENV_PATTERN: add  to negative lookbehind
   to prevent matching  inside
  when the latter is already redacted.

- GENERIC_HEADER_FIELD_PATTERN replacer: skip values starting with
   to preserve specific labels from earlier passes.

- Add 7 regression tests covering both finding categories.

* fix: address reviewer findings P1-P4

P1: Custom enumerable error properties now redacted in log.ts
  logError iterates all own enumerable properties on the original error
  and applies redactSensitiveInfo to string values and jsonRedactor to
  object values, preventing credential-bearing custom fields from leaking
  through the sanitized error. Regression tests added in log.test.ts.

P2: Soften single-source-of-truth claim; migrate easy call sites
  Header comment in redaction.ts updated to acknowledge that specialized
  scanners (secretScanner.ts, xaa.ts) are intentional exceptions.
  src/services/mcp/client.ts and src/services/mcp/auth.ts now use
  jsonRedactor for header redaction instead of ad-hoc key checks.

P3: Fix mock.restore cleanup in channelNotification.test.ts
  Cache-bust the real channelAllowlist module at describe-entry and
  re-register it in afterAll, following the pattern from bugfixes.test.ts.
  mock.restore alone does not clear mock.module overrides in Bun.

P4: Remove unused ChannelGateResult kinds
  Removed 'auth' and 'policy' from the skip kind union and removed
  corresponding dead branches in useManageMCPConnections.ts.

* fix: extract sanitizeError() to fix CI test fragility

The logError tests were failing in CI due to parallel test execution
racing on the module-level errorLogSink singleton. Extract the inline
sanitization logic into an exported sanitizeError() helper and test
that directly — it's pure, has no env-var or sink dependencies, and
doesn't interact with shared mutable state.

* fix: use Object.getPrototypeOf(err) instead of err as prototype in sanitizeError

Object.create(err) sets the original error instance as the prototype of the
sanitized copy, leaking non-enumerable own properties through the prototype
chain. Use Object.getPrototypeOf(err) instead so the prototype is the error
constructor's prototype (e.g. TypeError.prototype), preserving instanceof
checks without exposing the original error's non-enumerable fields.

Add a regression test verifying non-enumerable properties do not leak and
update the prototype-chain test to assert Object.getPrototypeOf result.

* fix: apply key-aware redaction and fail closed on non-serializable error props

- String properties: use jsonRedactor(key, value) instead of
  redactSensitiveInfo(value) so keys like apiKey with innocuous values
  (e.g. 'my-key') are still caught via SENSITIVE_FIELD_SUBSTRINGS.
- Object path: catch now replaces non-serializable/circular references
  with '[REDACTED]' instead of leaving the original object reference.
- Add 2 regression tests for key-aware redaction and fail-closed behavior.

* Update src/utils/log.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: redact bare auth header keys in JSON/header objects

- Add 'auth' to SENSITIVE_FIELD_SUBSTRINGS in src/utils/redaction.ts:109 to match URL/diagnostic redactors treatment of auth
- Add regression test for bare auth header keys in src/utils/diagnostics/redaction.test.ts:88

Co-authored-by: openhands <openhands@all-hands.dev>

* fix: narrow auth matching, redact nested transcript JSONL, fix channel skip message

* fix: address CodeRabbit nits — comment, hint, JSONL fallback redaction

* fix: key-aware malformed JSONL fallback and auth/x-auth in free-form text

* fix: strengthen redactJsonLines trailing rest redaction and auth test assertions

* fix: preserve non-JSON prefix in redactJsonLines fallback and redact it

* fix: tighten redactJsonLines prefix test to exact output assertion

* fix: redact MCP log sink payloads and errorStr before writing to disk

* fix: address P1 findings — URL #-in-password, ;-delimited query params, split channel trust-boundary

- Allow  in URL userinfo password on malformed-URL fallback path
  (new URL() fails when password contains fragment delimiter).
- Redact -delimited sensitive query params by splitting on both & and ;
  in redactMalformedQuery, plus redactSemicolonQueryParams post-processor
  for valid-URL output.
- Restore channelNotification.ts to upstream/main to fully split
  OAuth/org-policy trust-boundary changes from credential redaction PR.

* fix: update callers to match upstream/main function signatures

channelNotification.ts was restored to upstream/main to split
trust-boundary changes from the redaction PR. This commit updates
the three caller sites that previously passed extra arguments:

- ChannelsNotice.tsx: pass getSubscriptionType() + undefined to
  getEffectiveChannelAllowlist (needs 2 args upstream)
- interactiveHandler.ts, channelNotification.test.ts: drop 3rd
  pluginSource arg from findChannelEntry (takes 2 args upstream)

* fix: address reviewer findings — OAuth mock, notice states, marketplace disambiguation

P1: Mock getClaudeAIOAuthTokens and getSubscriptionType in channel
notification tests so they pass on CI where no real OAuth exists.

P2: Restore blocked-auth/org-policy notice states in ChannelsNotice.tsx
so the UI shows the correct blocker when gateChannelServer rejects
unauthenticated users or orgs without channelsEnabled.

P2: Add pluginSource disambiguation to findChannelEntry so same-name
plugin entries from different marketplaces are matched by runtime
source rather than first-match order. Add regression test with
non-matching marketplace first to cover the bug.

* fix: address reviewer findings — relay gate parity and allowlist regression test

- Replace filterPermissionRelayClients in interactiveHandler with inline
  gateChannelServer call so the relay predicate checks ALL gates including
  disabled-channel, auth, org policy, and approved-plugin allowlist, not
  just session entry + marketplace.
- Clean up unused imports (getAllowedChannels, parsePluginIdentifier,
  findChannelEntry, filterPermissionRelayClients).
- Add regression test: gateChannelServer rejects marketplace-matched
  plugin not on approved allowlist (full-gate path).

* fix: redact mixed semicolon secrets in valid-URL path and route OpenAI shim through centralized redactor

P1: Pre-redact semicolon-delimited sensitive query params from the raw
query string in redactUrlForDisplay BEFORE URLSearchParams encodes
; as %3B. Previously model=ok;token=SECRET leaked because
parsed.toString() reserialized to model=ok%3Btoken%3DSECRET, making
it invisible to the post-process pass.

P1: Route openaiShim's redactUrlForDiagnostics through the centralized
redactUrlForDisplay so the semicolon fix, malformed-URL fallback, and
all future redaction improvements apply to OpenAI-compatible
diagnostic logs too. Keep redactSecretValueForDisplay as an additional
safety net after the centralized pass.

Add 3 regression tests for mixed-separator queries.

* Update src/utils/redaction.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix: add fragment-query credential regression test and correct dev-channel gate comments

P2: Add regression test for redactUrlForDisplay with query-like credential
in fragment (e.g. #debug?token=SECRET). Fix raw-query pre-processing to
only extract query before the first #, preventing fragment content from
being treated as query parameters.

P3: Update comments in interactiveHelpers.tsx to match the actual gate
order — OAuth and org-policy gates still exist in gateChannelServer()
after restoring to upstream/main; the --dangerously-load-development-
channels flag only bypasses the allowlist gate.

* fix: add port+fragment+@ fallback test and restructure dev-channels dialog tests

* fix: registerDevChannels seam, bare-host #-in-password heuristic, and coverage restructure

* fix: add OAuth and org-policy gate test coverage

- Refactor auth module mock to use mutable variables per test
- Auth gate test: empty OAuth tokens -> kind:auth
- Policy gate test: team subscription without channelsEnabled -> kind:policy

* fix: prefer exact server channel entries before plugin disambiguation

- Return exact server-kind candidate first when candidates include both server and plugin entries with same name
- Added regression test covering mixed server/plugin --channels entries to ensure exact server opt-in is not overridden by plugin candidate
- This prevents a plugin marketplace mismatch from incorrectly rejecting a server the user explicitly selected via server:plugin:slack

* fix: only trust exact [REDACTED] placeholder in generic header field pattern

- Changed GENERIC_HEADER_FIELD_PATTERN to only bypass exact '[REDACTED]' canonical placeholder
- Prevents non-canonical placeholders like '[REDACTED_API_KEY]' or '[REDACTED_actual_secret]' from leaking through
- Updated tests to expect canonical '[REDACTED]' output for generic pattern

* fix: handle bare hosts in malformed URL userinfo fallback

- Added regex to recognize bare hostnames (with optional port) in the fragment heuristic
- Added tests for //alice:sec#ret@host and //alice:sec#ret@host:443

* fix: add relay dispatch path test for non-allowlisted plugin

- Added test using full gateChannelServer predicate in filterPermissionRelayClients
- Mirrors the exact relay dispatch path used in interactiveHandler
- Ensures marketplace-matched plugin not on allowlist is excluded from permission preview

* fix: enhance URL redaction logic to handle valid hosts before fragment

* fix: refine URL redaction logic to ensure valid host checks before fragment

* fix: enhance redaction logic to handle embedded URLs in free-form text

* fix: update redaction logic to remove user info from OpenAI base URL in diagnostic report

* fix: ensure findChannelEntry returns undefined when no exact matches are found

* fix: improve URL redaction logic to remove user info and ensure proper formatting

* fix: enhance redactDiagnosticUrl to preserve query-param values and trailing slashes

* fix: refine redaction logic to preserve meaningful path segments and handle trailing slashes correctly

* fix: enhance redactDiagnosticUrl to preserve literal path segments and handle trailing slashes correctly

* fix: preserve semicolon-delimited query params during redaction

* fix: update redaction logic to support semicolon-delimited query parameters

* fix: enhance redactUrlForDisplay to handle bare hosts and improve fragment redaction

* fix: enhance redactUrlForDisplay to correctly handle username-only userinfo with fragments

* fix: address privacy findings — URL redaction in jsonRedactor, base URL redaction, diagnostic object collapsing, structural channel previews, pluginSource telemetry

* fix: preserve falsey env-presence values in diagnostic redaction

- false, "", and 0 under isEnvPresenceKey keys are now preserved as-is
  instead of misrepresented as "[set]"
- Added regression test for absent/falsey env-presence inputs

* fix: address CodeRabbit findings — sync describe, heartbeat emitter, responsesBody filtering, dev entry precedence

* chore: remove stray Windows path artifact

* fix: update redaction import path in taskReport module

* fix: address CodeRabbit P1-P3 findings and rebase regressions

- F1: rebase onto upstream/main, fix taskReport.ts import path
- F2: Ollama native chat code recovered via rebase (6 functions)
- F3: &-truncation in credential regexes fixed via post-processing pass
- F4: 'tokens' added to jsonRedactor EXCLUDED_KEYS
- F5: redactHomePath case-sensitivity aligned with redactPathForStatus
- F6: credential metadata object preserved in issue report (sensitive-key check
  moved inside type branches)
- F7: heartbeat tests updated for pre-drain write behavior
- F8: reportTask test expects [REDACTED] (matches centralized output)
- rm: stray C:\repo\ Windows path artifact

* fix: address reviewer findings — generic regex &-handling and diagnostic secret-key masking

- Remove & from excluded char classes in 4 generic patterns so they consume
  full secret values (URL-query &-splitting belongs in redactUrlForDisplay).
- Remove now-obsolete &-tail post-processor pass.
- Remove credential from DIAGNOSTIC_SECRET_KEY_PATTERN so issue report
  credential metadata objects are traversed, not collapsed.
- Restore broad isDiagnosticSecretKey check before type dispatch in
  redactDiagnosticObjectInternal so objects/arrays under secret-marked keys
  (auth, password, token, etc.) are masked.
- Update issue report test baseUrl expectation (no trailing &mode=test after
  generic redactor consumes past &).

* fix: address reviewer findings — URL delimiter safety, jsonRedactor #-drop, embedded URL query redaction

- Restore &#; delimiters in generic pattern value classes (F1) so safe
  query tails (&mode=test) survive. Re-add &-tail post-processor for
  non-URL abc&def case.
- Gate redactUrlForDisplay in jsonRedactor to https?:// strings only (F2)
  to prevent #-drop on ordinary text like 'fails after #setup'.
- Add URL query redaction step to redactSensitiveInfo (F3) that extracts
  https?:// URLs from free-form text and routes them through
  redactUrlForDisplay, catching signature/sig params that generic patterns
  miss. Skip already-redacted URLs to avoid double-redaction.

* fix: add Cookie/Set-Cookie semicolon-safe redaction pass, tighten &-tail regex

* fix: COOKIE_PATTERN consume comma-joined multi-cookie values

* fix: address P2 findings — URL redact skip, pre-drain write promise, permission truthy check

* fix: update log.test.ts expectation, add protocol-relative URL support

* fix: enhance redaction for provider env-vars in URLs, preserve safe query params

* fix: enhance redaction for uppercase provider keys and cookie query params

* fix: enhance redaction for bare Bearer and JWT tokens in sensitive info

* fix: update report task test expectations for new redaction format

* fix: limit token exemption to numeric values, protect semicolon cookie query tails

* test: add tests for truncateForPreview to ensure sensitive data redaction

---------

Co-authored-by: Gravirei <gravirei@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2026-07-07 22:01:40 +08:00
2026-04-30 18:22:01 +08:00
2026-04-30 18:22:01 +08:00
2026-04-03 09:40:17 +08:00

OpenClaude

OpenClaude is an open-source coding-agent CLI for cloud and local model providers.

Use OpenAI-compatible APIs, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, Atomic Chat, and other supported backends while keeping one terminal-first workflow: prompts, tools, agents, MCP, slash commands, and streaming output.

PR Checks Release Discussions Discord X Security Policy License

OpenClaude is also mirrored to GitLawb: gitlawb.com/node/repos/z6MkqDnb/openclaude

Quick Start | Setup Guides | Providers | Source Build | VS Code Extension | Sponsors | Community

Sponsors

GitLawb logo Bankr.bot logo Atomic Chat logo Xiaomi MiMo logo Atlas Cloud logo
GitLawb Bankr.bot Atomic Chat Xiaomi MiMo Atlas Cloud

Star History

Star History Chart

Why OpenClaude

  • Use one CLI across cloud APIs and local model backends
  • Save provider profiles inside the app with /provider
  • Run with OpenAI-compatible services, Gemini, GitHub Models, Codex OAuth, Codex, Ollama, Atomic Chat, and other supported providers
  • Keep coding-agent workflows in one place: bash, file tools, grep, glob, agents, tasks, MCP, and web tools
  • Use the bundled VS Code extension for launch integration and theme support

Quick Start

Install

OpenClaude requires Node.js >=22.0.0 for npm installs and runtime. Bun is only needed for source builds and local development.

npm install -g @gitlawb/openclaude@latest

If you're on Arch Linux, you can install OpenClaude from the community-maintained AUR package:

paru -S openclaude

If the install later reports ripgrep not found, install ripgrep system-wide and confirm rg --version works in the same terminal before starting OpenClaude.

Verify / troubleshoot installed version:

openclaude --version
npm view @gitlawb/openclaude dist-tags
npm install -g @gitlawb/openclaude@latest

Start

openclaude

Inside OpenClaude:

  • run /provider for guided provider setup and saved profiles
  • run /onboard-github for GitHub Models onboarding

Note: OpenClaude does not automatically load project .env files. We recommend using the /provider command for setup, which saves provider profiles and credentials in .openclaude-profile.json. If you prefer environment variables, export them explicitly or run openclaude --provider-env-file .env for provider/setup variables. Export runtime/debug knobs from your shell or launcher.

Resume or fork a conversation

Resume an existing conversation by session ID, or continue the most recent conversation in the current directory:

openclaude --resume <session-id>
openclaude --continue

Add --fork-session to branch the conversation history into a new session ID instead of reusing the original transcript:

openclaude --resume <session-id> --fork-session
openclaude --continue --fork-session

Forking is conversation branching only. It does not create filesystem isolation, copy your working tree, or create a git worktree branch.

Background sessions

Run long non-interactive prompts detached from the current terminal:

openclaude --bg "fix failing tests"
openclaude --bg --name auth-refactor "refactor auth middleware"
openclaude ps
openclaude logs auth-refactor
openclaude logs auth-refactor -f
openclaude kill auth-refactor

Background sessions are local child processes. OpenClaude does not start a daemon or network service, and permission/provider/model/settings flags are passed to the child process the same way they are for a foreground --print run. Session metadata and logs are stored under the resolved OpenClaude config directory, usually ~/.openclaude/bg-sessions/; OPENCLAUDE_CONFIG_DIR can point OpenClaude somewhere else. CLAUDE_CONFIG_DIR is ignored for OpenClaude background-session storage. Session names can be reused after older sessions reach a terminal state; use the session ID to inspect older logs with the same name.

openclaude attach <id-or-name> currently reports the matching session and points to openclaude logs <id> -f; full terminal reattach is not implemented for local background sessions yet.

OpenClaude config cutover

OpenClaude stores its own config under ~/.openclaude and ~/.openclaude.json by default. It does not read ~/.claude, project .claude/ directories, or CLAUDE_CONFIG_DIR; new users can start with an empty OpenClaude config and do not need Claude Code installed.

If you previously used OpenClaude with .claude paths, migrate intentionally: copy only the settings, commands, agents, skills, scheduled tasks, or other files you personally created for OpenClaude into the matching .openclaude location. Do not blanket-copy .claude, and do not copy Claude Code credentials or auth files. For provider authentication, prefer running OpenClaude's provider setup again or exporting provider-specific environment variables.

Fastest OpenAI setup

macOS / Linux:

export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_API_KEY=sk-your-key-here
export OPENAI_MODEL=gpt-4o

openclaude

Windows PowerShell:

$env:CLAUDE_CODE_USE_OPENAI="1"
$env:OPENAI_API_KEY="sk-your-key-here"
$env:OPENAI_MODEL="gpt-4o"

openclaude

Fastest local Ollama setup

macOS / Linux:

export CLAUDE_CODE_USE_OPENAI=1
export OPENAI_BASE_URL=http://localhost:11434/v1
export OPENAI_MODEL=qwen2.5-coder:7b

openclaude

Windows PowerShell:

$env:CLAUDE_CODE_USE_OPENAI="1"
$env:OPENAI_BASE_URL="http://localhost:11434/v1"
$env:OPENAI_MODEL="qwen2.5-coder:7b"

openclaude

For Ollama, OpenClaude uses Ollama's native chat API and requests a 32768-token context window on each chat request so same-session history is not silently truncated by Ollama's OpenAI-compatible shim. Set OPENCLAUDE_OLLAMA_NUM_CTX or OLLAMA_CONTEXT_LENGTH if you need a different request-level context size. See Advanced Setup for verification with ollama ps.

Setup Guides

Beginner-friendly guides:

Advanced and source-build guides:

Supported Providers

Provider Setup Path Notes
OpenAI-compatible /provider or env vars Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible /v1 servers
Z.AI GLM Coding Plan /provider or OpenAI-compatible env vars Uses OPENAI_API_KEY at https://api.z.ai/api/coding/paas/v4 and defaults to glm-5.2
AI/ML API /provider or AIMLAPI_API_KEY (setup guide) Uses https://api.aimlapi.com/v1, auto-detects the OpenAI-compatible route from AIMLAPI_API_KEY, sends OpenClaude attribution headers, and discovers chat-capable models from the public /models catalog
Hicap /provider or OpenAI-compatible env vars Uses api-key auth, discovers models from unauthenticated /models, and supports Responses mode for gpt- models
Fireworks AI /provider or env vars First-class provider with 276 curated models (DeepSeek, Qwen, Llama, Gemma, and more); uses FIREWORKS_API_KEY
ClinePass /provider or env vars AI model gateway with usage limits (5hr, weekly, monthly); uses CLINE_API_KEY at https://api.cline.bot/api/v1
Gemini /provider or env vars Supports API key only
GitHub Models /onboard-github Interactive onboarding with saved credentials
Codex OAuth /provider Opens ChatGPT sign-in in your browser and stores Codex credentials securely
Codex /provider Uses existing Codex CLI auth, OpenClaude secure storage, or env credentials
Gitlawb Opengateway Startup default, /provider, or env vars Smart gateway at https://opengateway.gitlawb.com/v1; requires an API key from https://gitlawb.com/opengateway/keys and routes Xiaomi MiMo and GMI Cloud partner models by OPENAI_MODEL
OpenCode Zen /provider or env vars Pay-as-you-go AI gateway (48 models); uses OPENCODE_API_KEY via https://opencode.ai/zen/v1; shared key with OpenCode Go
OpenCode Go /provider or env vars $10/mo subscription for open models (13 models); uses OPENCODE_API_KEY via https://opencode.ai/zen/go/v1; shared key with OpenCode Zen
Xiaomi MiMo /provider or env vars OpenAI-compatible API at https://mimo.mi.com; uses MIMO_API_KEY and defaults to mimo-v2.5-pro
NEAR AI /provider or env vars Unified gateway (Claude, GPT, Gemini + TEE open models); uses NEARAI_API_KEY at https://cloud-api.near.ai/v1
Ollama /provider or env vars Local inference with no API key
Atomic Chat /provider, env vars, or bun run dev:atomic-chat Local Model Provider; auto-detects loaded models
Bedrock / Vertex / Foundry env vars Anthropic-family cloud routes; Vertex is for Claude on Vertex AI, not arbitrary Model Garden models

What Works

  • Tool-driven coding workflows: Bash, file read/write/edit, grep, glob, agents, tasks, MCP, and slash commands
  • Streaming responses: Real-time token output and tool progress
  • Tool calling: Multi-step tool loops with model calls, tool execution, and follow-up responses
  • Images: URL and base64 image inputs for providers that support vision
  • Provider profiles: Guided setup plus saved user-level provider profile support
  • Local and remote model backends: Cloud APIs, local servers, and Apple Silicon local inference
  • Codebase intelligence (repo map): Structural map of the repository ranked by PageRank importance, auto-injected into context when the REPO_MAP flag is enabled or the REPO_MAP environment variable is set. Inspect with /repomap (2048-token default). See docs/repo-map.md for details.

Provider Notes

OpenClaude supports multiple providers, but behavior is not identical across all of them.

  • Anthropic-specific features may not exist on other providers
  • Tool quality depends heavily on the selected model
  • Smaller local models can struggle with long multi-step tool flows
  • Some providers impose lower output caps than the CLI defaults, and OpenClaude adapts where possible
  • AI/ML API uses the OpenAI-compatible route, defaults to gpt-4o, and only surfaces chat-capable models from its public catalog
  • Gitlawb Opengateway is the fresh-install startup default and requires an API key from https://gitlawb.com/opengateway/keys. It uses one OpenAI-compatible base URL; switch between mimo-* and google/gemini-3.1-flash-lite-preview with /model, and do not pin the base URL to /v1/xiaomi-mimo.
  • Z.AI GLM Coding Plan uses https://api.z.ai/api/coding/paas/v4 with glm-5.2 by default. Use glm-5.2?reasoning=high for enhanced reasoning, glm-5.2?reasoning=xhigh to request Z.AI reasoning_effort=max, or glm-5.2?thinking=disabled for faster direct answers.
  • Xiaomi MiMo uses api-key header auth on the direct OpenAI-compatible route and currently does not support /usage reporting in OpenClaude

GitHub Copilot sub-agent optimization

When CLAUDE_CODE_USE_GITHUB=1, OpenClaude serializes sub-agent execution to reduce GitHub Copilot Premium Request consumption. Default behavior is GITHUB_COPILOT_MAX_SUBAGENTS=1 (synchronous, one sub-agent at a time). Tuning vars (all optional):

Var Effect
GITHUB_COPILOT_MAX_SUBAGENTS=0 Suppress sub-agents entirely (sub-agents throw an error).
GITHUB_COPILOT_MAX_SUBAGENTS=1 Force synchronous execution. Default.
GITHUB_COPILOT_MAX_SUBAGENTS=2..10 Parsed/clamped but not enforced differently from =1 (any positive cap = synchronous).
GITHUB_COPILOT_ALLOW_SUBAGENTS=1 Re-enable parallel/background sub-agents, overriding the cap.
GITHUB_COPILOT_FORCE_SYNC_SUBAGENTS=1 Force synchronous execution regardless of cap.
GITHUB_COPILOT_OPTIMIZATION_DISABLED=1 Disable all of the above; sub-agents run as before this feature.

The is_async field reported in the tengu_agent_tool_selected event and the agent metadata now reflects the final execution mode (i.e., false when synchronous is forced). See .env.example for the full descriptions.

For best results, use models with strong tool/function calling support.

Agent step limits

Custom agents can define maxSteps as a positive integer to cap how many tool-use steps a sub-agent may execute. When the limit is reached, OpenClaude stops additional tool calls and asks the sub-agent for a concise final summary covering completed work, findings, remaining tasks, and whether another run is needed. Omitting maxSteps, or setting it to an invalid value such as 0 or malformed input, preserves the default unlimited behavior.

---
name: bounded-researcher
description: Use for focused research with bounded tool use
maxSteps: 8
---

You are a focused research agent.

Agent Routing

OpenClaude can route different agents to different models through settings-based routing. This is useful for cost optimization or splitting work by model strength.

Add to ~/.openclaude.json:

{
  "agentModels": {
    "deepseek-v4-flash": {
      "base_url": "https://api.deepseek.com/v1",
      "api_key": "sk-your-key"
    },
    "zai-default": {
      "model": "glm-5.2",
      "base_url": "https://api.z.ai/api/coding/paas/v4",
      "api_key": "sk-your-key"
    },
    "gpt-4o": {
      "base_url": "https://api.openai.com/v1",
      "api_key": "sk-your-key"
    }
  },
  "agentRouting": {
    "Explore": "deepseek-v4-flash",
    "Plan": "gpt-4o",
    "general-purpose": "gpt-4o",
    "frontend-dev": "zai-default",
    "default": "gpt-4o"
  }
}

When no routing match is found, the global provider remains the fallback.

agentRouting values and explicit Agent tool model overrides match keys in agentModels. By default, that key is also the model string sent to the provider. Set agentModels.<key>.model when you want a local route key such as zai-default to call a different provider model name such as glm-5.2.

Note: /provider changes the global/parent provider for your current session. agentModels and agentRouting are specifically for configuring per-agent provider overrides while keeping the parent session unchanged.

Note: api_key values in settings.json are stored in plaintext. Keep this file private and do not commit it to version control.

Model-only routes (same provider): Omit base_url and api_key to run an agent on a different model using your current provider's endpoint and key — no credential duplication:

{
  "agentModels": {
    "mini": { "model": "gpt-5-mini" }
  },
  "agentRouting": {
    "verification": "mini"
  }
}

Built-in agents are routable by their type name. Useful keys: verification (the read-only auditor that runs before completion), Explore, and Plan. For example, "agentRouting": { "verification": "mini" } runs the verifier on gpt-5-mini while your main session stays on its model. Absent any entry, the verifier inherits the main-loop model.

Web Search and Fetch

By default, WebSearch works on non-Anthropic models using DuckDuckGo. This gives GPT-4o, DeepSeek, Gemini, Ollama, and other OpenAI-compatible providers a free web search path out of the box.

Note: DuckDuckGo fallback works by scraping search results and may be rate-limited, blocked, or subject to DuckDuckGo's Terms of Service. If you want a more reliable supported option, configure Firecrawl.

For Anthropic-native backends and Codex responses, OpenClaude keeps the native provider web search behavior.

WebFetch works, but its basic HTTP plus HTML-to-markdown path can still fail on JavaScript-rendered sites or sites that block plain HTTP requests.

Set a Firecrawl API key if you want Firecrawl-powered search/fetch behavior:

export FIRECRAWL_API_KEY=your-key-here

With Firecrawl enabled:

  • WebSearch can use Firecrawl's search API while DuckDuckGo remains the default free path for non-Claude models
  • WebFetch uses Firecrawl's scrape endpoint instead of raw HTTP, handling JS-rendered pages correctly

Free tier at firecrawl.dev includes 500 credits. The key is optional.


Headless gRPC Server

OpenClaude can be run as a headless gRPC service, allowing you to integrate its agentic capabilities (tools, bash, file editing) into other applications, CI/CD pipelines, or custom user interfaces. The server uses bidirectional streaming to send real-time text chunks, tool calls, and request permissions for sensitive commands.

1. Start the gRPC Server

Start the core engine as a gRPC service on localhost:50051:

npm run dev:grpc

Configuration

Variable Default Description
GRPC_PORT 50051 Port the gRPC server listens on
GRPC_HOST localhost Bind address. Use 0.0.0.0 to expose on all interfaces (not recommended without authentication)

2. Run the Test CLI Client

We provide a lightweight CLI client that communicates exclusively over gRPC. It acts just like the main interactive CLI, rendering colors, streaming tokens, and prompting you for tool permissions (y/n) via the gRPC action_required event.

In a separate terminal, run:

npm run dev:grpc:cli

Note: The gRPC definitions are located in src/proto/openclaude.proto. You can use this file to generate clients in Python, Go, Rust, or any other language.


Source Build And Local Development

Use Node.js >=22.0.0 and Bun 1.3.13 or newer for source builds.

bun install
bun run build
node dist/cli.mjs

Helpful commands:

  • bun run dev
  • bun test
  • bun run test:coverage
  • bun run security:pr-scan -- --base origin/main
  • bun run smoke
  • bun run doctor:runtime
  • bun run verify:privacy
  • focused bun test ... runs for the areas you touch

Testing And Coverage

OpenClaude uses Bun's built-in test runner for unit tests.

Run the full unit suite:

bun test

Generate unit test coverage:

bun run test:coverage

Open the visual coverage report:

open coverage/index.html

If you already have coverage/lcov.info and only want to rebuild the UI:

bun run test:coverage:ui

Use focused test runs when you only touch one area:

  • bun run test:provider
  • bun run test:provider-recommendation
  • bun test path/to/file.test.ts

Recommended contributor validation before opening a PR:

  • bun run build
  • bun run smoke
  • bun run test:coverage for broader unit coverage when your change affects shared runtime or provider logic
  • focused bun test ... runs for the files and flows you changed

Coverage output is written to coverage/lcov.info, and OpenClaude also generates a git-activity-style heatmap at coverage/index.html.

Repository Structure

  • src/ - core CLI/runtime
  • scripts/ - build, verification, and maintenance scripts
  • docs/ - setup, contributor, and project documentation
  • vscode-extension/openclaude-vscode/ - VS Code extension
  • .github/ - repo automation, templates, and CI configuration
  • bin/ - CLI launcher entrypoints

VS Code Extension

The repo includes a VS Code extension in vscode-extension/openclaude-vscode for OpenClaude launch integration, provider-aware Control Center, in-editor chat, theme support, and optional Microsoft Foundry / Azure OpenAI configuration (endpoint, API version, deployment, API key via Secret Storage) injected into launched terminals. See that folders README.

Security

If you believe you found a security issue, see SECURITY.md.

Community

Contributing

Contributions are welcome.

For larger changes, open an issue first so the scope is clear before implementation. Helpful validation commands include:

  • bun run build
  • bun run test:coverage
  • bun run smoke
  • focused bun test ... runs for files and flows you changed

Disclaimer

OpenClaude is an independent community project and is not affiliated with, endorsed by, or sponsored by Anthropic.

OpenClaude originated from the Claude Code codebase and has since been substantially modified to support multiple providers and open use. "Claude" and "Claude Code" are trademarks of Anthropic PBC. See LICENSE for details.

License

MIT for OpenClaude contributors' modifications; the derived Claude Code remains Anthropic's. See more.

S
Description
runs anywhere. uses anything
Readme
83 MiB
Languages
TypeScript 99%
JavaScript 0.7%
Astro 0.2%