Skip to content

fix(forge-cli): optimizer stop no longer restores the proxy's own dead URL - #540

Open
ratnesh-maurya wants to merge 1 commit into
initializ:mainfrom
ratnesh-maurya:fix/optimizer-stop-self-poisoned-base-url
Open

ratnesh-maurya wants to merge 1 commit into
initializ:mainfrom
ratnesh-maurya:fix/optimizer-stop-self-poisoned-base-url

Conversation

@ratnesh-maurya

Copy link
Copy Markdown
Member

Fixes #539.

What & why

forge optimizer start captured whatever ANTHROPIC_BASE_URL sat in ~/.claude/settings.json as prev_base_url, with no check that forge itself had written it. A second start — including the adopt path, where a proxy is already running and the settings are already wired — therefore recorded http://127.0.0.1:8787, the optimizer's own address, as the value to revert to. stop then restored it faithfully:

API Error: Connection refused — a firewall or proxy may be blocking it (ECONNREFUSED)

Every claude in every terminal now pointed at a dead proxy, and the error blames a firewall rather than forge. Recovery required hand-editing ~/.claude/settings.json.

The same line hit two more cases:

  • ENABLE_TOOL_SEARCH had the identical flaw one line down — prev_tool_search became forge's own "true", so stop left tool search enabled instead of removing the key.
  • A real gateway was lost outright. For a user whose base URL was an org gateway (optimizer.upstream, optimizer: forward an explicit upstream from start (flag + settings chaining) #523), the second start overwrote the saved gateway with the proxy address, so stop could never put it back.

This breaks the invariant the package comment already claims: "stop reverts it precisely, so claude never ends up pointed at a dead proxy."

Change

sanitizePrevWiring (pure, in optimizer_daemon.go) discards a captured value that forge wrote itself, and carries the prior daemon state's genuine originals forward instead:

prevBase, prevTS := sanitizePrevWiring(
    captureStr(env, "ANTHROPIC_BASE_URL"),
    captureStr(env, "ENABLE_TOOL_SEARCH"),
    listen, prior,
)
  • Self-written detection (forgeWiredBaseURL) matches both the current listen address and the one the prior state recorded, so a start on a different --listen still recognizes stale wiring rather than treating the old proxy URL as a user value. Trailing slash tolerated.
  • The pair moves together. A self-written base URL implies the ENABLE_TOOL_SEARCH read back is ours too, so both are replaced from the prior state rather than trusted independently — the value "true" alone cannot distinguish forge's write from the user's.
  • runOptimizerStart loads the prior state before wiring (readOptDaemonState); nil on a clean start, in which case a self-written value degrades to "key was absent", which is correct.

Same class of guard as the existing resolveChildUpstream self-loop check (#523) — forge must not mistake its own address for the user's configuration, on the restore path as well as the forward path.

Tests

End-to-end against a sandboxed HOME, start → start → stop, nothing set beforehand:

env after stop
main ANTHROPIC_BASE_URL: http://127.0.0.1:8797, ENABLE_TOOL_SEARCH: true
this PR clean — both keys removed

With a pre-existing https://gw.corp.example / ENABLE_TOOL_SEARCH: false, the gateway survives the double start and is restored exactly.

Unit coverage in optimizer_daemon_test.go — seven table cases plus a named regression test for the reported break:

  • absent key stays absent; a real gateway is captured untouched
  • our own listen is never recorded, with and without a trailing slash
  • a re-start carries the prior gateway forward
  • a start on a new port still spots the old wiring
  • an unrelated 127.0.0.1:3000 is still treated as a genuine previous value
  • TestSanitizePrevWiring_StopNeverRestoresDeadProxy — start → start → stop leaves neither key behind

gofmt, go vet, golangci-lint run ./forge-cli/... (0 issues) and the full forge-cli suite are green.

Not in this PR

The stale MCP registration noted under "Related" in #539: stop deregisters forge-optimizer, but only via claude mcp remove, so a failure there leaves the entry behind and every later session reports forge-optimizer (ECONNREFUSED). Separate change.

Upgrade note

The fix prevents a daemon state file from being poisoned; it does not repair one already poisoned. A user whose ~/.forge/optimizer-daemon.json currently holds "prev_base_url": "http://127.0.0.1:8787" should set that field to null before the next stop, or delete ANTHROPIC_BASE_URL from ~/.claude/settings.json afterward.

Checklist

  • Bug fix
  • Tests pass for affected modules (go test ./...)
  • Code is formatted (gofmt -w)
  • Linter passes (golangci-lint run)
  • go vet reports no issues
  • No new egress domains added

…restore

`optimizer start` captured whatever ANTHROPIC_BASE_URL was in
~/.claude/settings.json as prev_base_url, with no check that forge had
written it. A second start (including the adopt path, where a proxy is
already running and the settings are already wired) therefore saved
http://127.0.0.1:8787 — the optimizer's own address — as the thing to
revert to. `stop` then restored it faithfully, leaving every `claude`
pointed at a dead proxy:

    API Error: Connection refused (ECONNREFUSED)

ENABLE_TOOL_SEARCH had the same flaw: prev_tool_search became forge's own
"true", so stop left tool search enabled instead of removing the key. A
user whose real base URL was a gateway lost it outright — the second start
overwrote the saved gateway, so stop could never put it back.

sanitizePrevWiring now discards a captured value that forge wrote itself —
matching our current listen addr, or the one the prior daemon state
recorded, so a start on a different --listen still spots stale wiring — and
carries the prior state's genuine originals forward instead. Because a
self-written base URL means the tool-search value is ours too, both are
replaced together rather than trusted independently.

Fixes initializ#539
@ratnesh-maurya

Copy link
Copy Markdown
Member Author

@initializ-mk could you take a look when you have a moment?

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.

optimizer stop leaves a dead ANTHROPIC_BASE_URL in ~/.claude/settings.json, breaking every claude with ECONNREFUSED

1 participant