Skip to content

fix(agents): detect and warn on ANTHROPIC_BASE_URL override in ~/.claude/settings.json - #425

Open
alex-budanov wants to merge 20 commits into
codemie-ai:mainfrom
alex-budanov:EPMCDME-10988
Open

fix(agents): detect and warn on ANTHROPIC_BASE_URL override in ~/.claude/settings.json#425
alex-budanov wants to merge 20 commits into
codemie-ai:mainfrom
alex-budanov:EPMCDME-10988

Conversation

@alex-budanov

@alex-budanov alex-budanov commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes EPMCDME-10988. `codemie-code` injects `ANTHROPIC_BASE_URL` via the active profile, but Claude Code silently overrides that value if `~/.claude/settings.json` also contains `ANTHROPIC_BASE_URL`. The startup banner showed the profile URL while the session used a different endpoint — invisible to the user.

This change detects the override at startup and prints a visible warning showing both URLs before the session starts. It also detects `ANTHROPIC_MODEL` overrides (scope expanded from the original spec — approved).

Changes

  • `src/agents/plugins/claude/settings-conflict.ts` (new) — pure async detection helper; reads `~/.claude/settings.json`, returns `ConflictInfo | null` (covers `ANTHROPIC_BASE_URL` and `ANTHROPIC_MODEL`)
  • `src/agents/plugins/claude/claude.plugin.ts` — calls `detectSettingsConflict` in `beforeRun`; prints `chalk.yellow` warning to stderr; wrapped in `try/catch` so a detection failure never aborts startup; sanitizes URL and model values against ANSI/terminal injection
    • Security hardening: added C1-form OSC `\x9d` to DCS pre-strip regex (CR-001); clears `url.hash` in `safeUrl` (CR-002); wraps `conflict.profileModel` in `safeUrl()` before output (CR-003)
  • `src/agents/plugins/claude/tests/settings-conflict.test.ts` (new) — 16 unit tests covering all edge cases including empty-string URL, filesystem errors, and model conflict detection
  • `src/agents/plugins/claude/tests/claude.plugin.conflict.test.ts` (new) — 17 integration tests covering warning emission, empty-string fallback, try/catch degradation, ANSI stripping, and regression guards for CR-001/CR-002/CR-003 injection vectors
  • `.ai-run/guides/usage/project-config.md` — documents `~/.claude/settings.json` precedence and how to resolve it
  • `vitest.config.ts` — `hookTimeout` increased 10s → 30s (WSL2 cold-start on `/mnt/c`)

Testing

Automated (33 tests):

  • `npx vitest run src/agents/plugins/claude/tests/settings-conflict.test.ts` — 16 passed
  • `npx vitest run src/agents/plugins/claude/tests/claude.plugin.conflict.test.ts` — 17 passed
  • `npm run typecheck` — clean
  • `npm run build` — clean

Manual E2E smoke test:

Since `codemie-code` is not yet in production and the full CLI is interactive, the warning was verified by importing `ClaudePluginMetadata.lifecycle.beforeRun` directly from the built `dist/` in a minimal Node.js script:

  1. Built the project (`npm install && npm run build`)
  2. Injected a conflicting URL into `~/.claude/settings.json` via Python one-liner
  3. Ran a `smoke.mjs` script that calls `beforeRun` with the profile URL in `env` — no network calls, exits in under a second
  4. Confirmed the ⚠️ warning appeared with the correct profile URL and active URL
  5. Updated `~/.claude/settings.json` to match the profile URL, re-ran — confirmed no warning
  6. Restored `~/.claude/settings.json` to its original state

Checklist

  • Code follows project standards
  • Tests added (33 tests)
  • No secrets or unsafe logging
  • Architecture boundaries respected (Plugin layer only)
  • Documentation updated

Closes EPMCDME-10988

🤖 Generated with Claude Code

@yanaSelin yanaSelin left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review — EPMCDME-10988 (3 findings: 1 critical, 2 major)

All three review lenses ran (blind, edge-case, acceptance). Two findings are straight patches; one needs a scope decision from the product owner before it can be resolved.

See inline comments for details.

Comment thread src/agents/plugins/claude/settings-conflict.ts Outdated
Comment thread src/agents/plugins/claude/__tests__/settings-conflict.test.ts Outdated
Comment thread src/agents/plugins/claude/claude.plugin.ts Outdated
Comment thread src/agents/plugins/claude/settings-conflict.ts
@alex-budanov

Copy link
Copy Markdown
Contributor Author

Second-pass fixes (CR-001 / CR-002 / CR-003 from tech lead review)

All three items addressed via TDD (test written RED, confirmed fail, then implementation to GREEN):

Finding Fix
CR-001 docs project-config.md now shows the correct env block JSON snippet
CR-001 regression Added test: root-level ANTHROPIC_BASE_URL (no env nesting) → null
CR-002 safeUrl now uses strip-ansi + C1 blocklist [\x00-\x1f\x7f-\x9f]; covers single-byte C1 CSI (\x9b) the hand-rolled regex missed
CR-003 Added !settings || typeof settings !== 'object' || Array.isArray(settings) guard after JSON.parse; tests for JSON null and JSON array inputs

19 tests passing, no lint/typecheck warnings.

Comment thread src/agents/plugins/claude/claude.plugin.ts
Comment thread src/agents/plugins/claude/settings-conflict.ts
@alex-budanov

Copy link
Copy Markdown
Contributor Author

Security hardening follow-up (commit 8317768)

Three terminal-injection gaps identified during code review were patched in a follow-up commit:

CR-001 — OSC C1-form \x9d not stripped
The DCS pre-strip regex covered ESC-form and most C1-form introducers but omitted \x9d (single-byte C1 OSC). A crafted settingsUrl containing \x9d0;payload\x07 would have its control byte stripped but the payload text leaked as printable ASCII. Fixed by adding \x9d to the character class.

CR-002 — url.hash not cleared in safeUrl
url.username, url.password, and url.search were explicitly cleared, but url.hash was not. A fragment like #injected-section survives both ANSI strip and the ASCII allowlist and renders verbatim in the warning output. Fixed by adding url.hash = '' to the clearing block.

CR-003 — conflict.profileModel not sanitized before terminal output
settingsUrl, profileUrl, and settingsModel are all passed through safeUrl() before console.error, but conflict.profileModel (sourced from env.ANTHROPIC_MODEL) was interpolated raw. A crafted ANTHROPIC_MODEL value containing ANSI escape sequences would reach the terminal unfiltered. Fixed by wrapping it in safeUrl().

Three regression tests added to claude.plugin.conflict.test.ts covering each vector. All 17 tests pass.

Aleksandr Budanov and others added 19 commits July 28, 2026 17:12
…ection

Design for detecting ANTHROPIC_BASE_URL in ~/.claude/settings.json that would
silently override the active profile URL. Proposes a new settings-conflict.ts
helper called from claude.plugin.ts beforeRun with chalk-formatted warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
3-task TDD plan: settings-conflict.ts helper (6 unit tests), beforeRun wiring
(3 integration tests), and project-config.md documentation update.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire detectSettingsConflict into lifecycle.beforeRun: emits a chalk.yellow
multi-line warning to stderr when ~/.claude/settings.json contains an
ANTHROPIC_BASE_URL that differs from (or is absent from) the profile env.

Uses a dynamic import of settings-conflict.js (matching the existing
statusline-installer pattern) to avoid auto-mock initialisation slowness
in the WSL2 test environment. hookTimeout is raised from 10 s to 30 s in
vitest.config.ts to accommodate the Windows-filesystem cold-start latency;
this also resolves pre-existing statusline test timeouts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Wrap detectSettingsConflict import+call in try/catch so a thrown
  exception (e.g. os.homedir() failing in containers) degrades
  gracefully with logger.warn instead of aborting session startup
- Strip C0/C1 control characters and ANSI CSI sequences from URL
  values before chalk interpolation to prevent terminal injection
  via a crafted ~/.claude/settings.json
- Replace ?? with || for profileUrl fallback so an explicit
  empty-string profile URL also shows "(not set)" in the warning

Extends the integration test suite from 3 to 6 tests to cover:
- try/catch degradation path (logger.warn, no throw)
- empty-string profileUrl fallback
- C0/ANSI stripping in URL values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Update spec to reflect the implementation decisions made during SDD:
- displayWarningMessage replaced by console.error(chalk.yellow(...))
- Integration tests go in dedicated conflict.test.ts file
- Clarify resolveHomeDir import path pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All phases complete: implementation, code review (with fix-up), QA gates
passed. Work item status updated from 'Ready for dev' to 'Ready for review'.

Branch: EPMCDME-10988
Commits: ffcbbc8..282c66a (8 commits)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Cover empty-string ANTHROPIC_BASE_URL (CR-004) and readFile EACCES
rejection (CR-005) identified in tech lead code review.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add git clone from fork as the primary path for users without the repo
- Replace hardcoded WSL path in smoke.mjs with $REPO_DIR shell variable
- Clarify that origin/fork remote layout is for existing clones only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
docs/superpowers/runs/ is gitignored — this file was accidentally
force-added. It is a local pipeline artifact, not part of the PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CR-001 [CRITICAL]: read ANTHROPIC_BASE_URL from settings.env block
Claude Code stores env vars under a nested `env` object in settings.json,
not at the root. `settings.ANTHROPIC_BASE_URL` was always undefined so
detection never fired. Fixed to read `settings.env.ANTHROPIC_BASE_URL`.
Updated all test mocks to use `{ env: { ... } }` schema so they validate
the spec rather than the broken implementation.

CR-002 [MAJOR]: fix CSI regex alternation order to prevent bracket residue
The C0 catch-all `[\x00-\x1f]` was consuming `\x1b` before the full CSI
pattern could match, leaving `[31mFORGED[0m` visible in the terminal.
Moved the CSI alternative first so the entire escape sequence is consumed
atomically. Added a failing test for the injected-residue attack vector.

Also: add `vi.mock('../../../core/BaseAgentAdapter.js')` to the conflict
integration test to prevent SSO provider auto-registration from triggering
network I/O during test setup (caused 30s timeouts in WSL/CI environments).
CR-001: update project-config.md to document env block schema for
ANTHROPIC_BASE_URL; add regression test asserting root-level key returns null.

CR-002: replace hand-rolled CSI regex in safeUrl with strip-ansi +
C1 control-range blocklist ([\x00-\x1f\x7f-\x9f]); add test for single-byte
C1 CSI form (\x9b) that the old regex missed.

CR-003: guard against JSON.parse returning null or a non-object value
in detectSettingsConflict; add tests for JSON null and JSON array inputs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oofing

Switch from a blocklist to a layered defence:
1. DCS pre-strip — remove full \x1bP/\x1bX/\x1b^/\x1b_ sequences including
   payload before strip-ansi; ansi-regex only strips the 2-byte introducer,
   leaving any ASCII payload intact.
2. ASCII allowlist [^\x20-\x7e] — replaces the old C0/C1 blocklist; also
   covers Bidi override chars, soft hyphen (U+00AD), zero-width chars,
   combining marks, and all other non-ASCII Unicode vectors in one rule.
3. URL userinfo guard — new URL() parse detects credentials in the URL
   (the @-trick: https://trusted@evil.com routes to evil.com); userinfo is
   stripped and [credentials removed] is prepended so the real host is visible.

Two new TDD tests added (10 total in the conflict test suite).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tests

CR-NEW-001: the hardcoded '(not set — direct Anthropic API)' fallback was
passed through safeUrl, whose ASCII allowlist strips U+2014 (em dash) and
collapses the surrounding spaces to a double space. Fixed by bypassing safeUrl
for the known-safe constant — only user-controlled values need sanitising.

Four tests added (14 total):
- em dash preserved in hardcoded fallback (regression guard for CR-NEW-001)
- DCS with no terminator — validates the |$ end-of-string fallback path
- C1 DCS form (\x90...\x07) — single-byte introducer path in pre-strip regex
- URL userinfo with password field (user:pass@host) — covers url.password branch

Also adds an inline comment on the DCS regex identifying the ESC-form and
C1-form character groups (\x90 \x98 \x9e \x9f) for future maintainers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Expand credentials-removal branch to also clear url.search and
trigger it when query params are present. Prevents api_key= style
secrets leaking into console.error and CI log aggregators.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ay model in conflict warning

Extends ConflictInfo with optional settingsModel/profileModel fields.
detectSettingsConflict now checks settings.env.ANTHROPIC_MODEL alongside
ANTHROPIC_BASE_URL. The CLI warning block conditionally shows a model row
when a model conflict is present, satisfying AC-3 ('display actual
endpoint/model being used').

16 unit tests passing (5 new for model-conflict paths).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…vectors

- CR-001: add \x9d to DCS pre-strip regex to cover C1-form OSC introducer
- CR-002: clear url.hash in safeUrl to prevent fragment-payload leakage
- CR-003: wrap conflict.profileModel in safeUrl() before terminal output
- Add three regression tests covering each injection vector

All 17 tests in claude.plugin.conflict.test.ts pass.
Hardcoded POSIX string caused Test (Windows) to fail: path.join on
Windows converts '/home/testuser/claude' to '\home\testuser\claude',
so the assertion never matched. Computing SETTINGS_PATH with join()
makes the expected value platform-correct on both Ubuntu and Windows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants