Skip to content

fix(llm): isolate Claude Code CLI calls from the user's sessions - #127

Merged
0xMMA merged 1 commit into
mainfrom
fix/claude-cli-isolation
Oct 11, 2026
Merged

0xMMA merged 1 commit into
mainfrom
fix/claude-cli-isolation

Conversation

@0xMMA

@0xMMA 0xMMA commented Oct 11, 2026

Copy link
Copy Markdown
Owner

What

The Claude Code CLI provider now runs every spawn isolated from the user's own Claude Code sessions. That covers completions as well as the --version / auth status probes.

  • Working directory: each spawn runs in an empty directory KeyLint owns, under the user cache dir: %LocalAppData%\KeyLint\claude-cli, ~/.cache/KeyLint/claude-cli, or ~/Library/Caches/KeyLint/claude-cli.
    • Without a user cache dir, it falls back to a private os.MkdirTemp directory created for the process (random name, mode 0700).
    • If that also fails, the call fails with a clear error instead of running in the inherited directory.
  • Auto-memory off: CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 is set on every spawn, and an inherited value cannot override it.
  • Stripped variables: inherited ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL, ANTHROPIC_SMALL_FAST_MODEL, MAX_THINKING_TOKENS and CLAUDE_CODE_MAX_OUTPUT_TOKENS are removed, the same way the credential variables already were. The log line now reads "environment variables removed".

Why

--setting-sources "" does not stop Claude Code's auto-memory. With KeyLint's working directory inherited (the install folder, or a developer's checkout), the memory file of the matching ~/.claude/projects/<slug> was injected into every Fix and Pyramidize call. This was measured on 2026-10-04 while building the CLI eval in #105.

It is a privacy problem: the user's notes from their own Claude Code sessions went along with every grammar fix. It is also a correctness problem, because those notes can change the answer. Alias-remapping variables in the user's shell could likewise make the CLI answer with a model other than the one KeyLint asked for.

Shared fallback dropped: an earlier draft fell back to a fixed os.TempDir()/keylint-claude-cli. That path is shared and predictable, so another account on the machine could create it first. The fallback is now a per-process os.MkdirTemp directory. Erroring out was the alternative, but a missing cache dir is rare and the private directory is just as neutral. Only when no directory can be made at all does the call fail; the system-prompt temp file would fail in that situation anyway.

Not verified

  • Whether the CLI still reads user-level instructions: ~/.claude/CLAUDE.md, or a CLAUDE.md in a parent of the cache dir such as the home directory.
  • What was shown is only the auto-memory leak and that the neutral working directory plus CLAUDE_CODE_DISABLE_AUTO_MEMORY close it.
  • Checking the user-level files would need a marker file in the real ~/.claude, which this PR does not touch.

How verified

  • New tests in internal/llm (stub CLI, no model calls):
    • The completion runs in the neutral directory with auto-memory forced on.
    • The remapping and thinking variables never reach the CLI.
    • The status probes run in the neutral directory. A mutation check confirmed this test fails when the probe's cmd.Dir is dropped.
    • The private-temp fallback: not the fixed shared name, mode 0700, reused across calls.
    • With no directory available, the call fails and the CLI never runs.
  • Test hygiene: TestMain points the cache dir at a temp dir on every OS, so no test writes to the real cache.
  • go vet -tags gtk3 ./..., go test -tags gtk3 ./internal/... and go build -tags gtk3 -o bin/KeyLint . all pass.
  • No Go service signature changed, so no wire gen / bindings run.
  • Not run: no manual Windows check.

#105 (the CLI eval) is rebased on top of this branch and depends on it. Merge this first.

--setting-sources "" does not stop Claude Code's auto-memory: with
KeyLint's working directory inherited, the memory file of the matching
~/.claude/projects/<slug> was injected into every Fix and Pyramidize
call (measured 2026-10-04, while building the CLI eval in #105).

- Every spawn, the status probes included, runs in an empty
  KeyLint-owned directory under the user cache dir. Without a cache dir
  it uses a private os.MkdirTemp directory for the process (random
  name, 0700) instead of a shared fixed path; if that fails too the call
  fails with a clear error rather than inheriting the cwd.
- CLAUDE_CODE_DISABLE_AUTO_MEMORY=1 is set; an inherited value cannot
  turn it back on.
- Inherited alias-remapping (ANTHROPIC_DEFAULT_*_MODEL,
  ANTHROPIC_SMALL_FAST_MODEL), thinking-budget and output-cap variables
  are stripped like the credential ones.
- Tests point the cache dir at a temp dir on every OS.

Co-Authored-By: Claude <noreply@anthropic.com>
0xMMA added a commit that referenced this pull request Oct 11, 2026
Review follow-ups on the two-judge instrument:

- Per-judge columns are keyed by the ID that answered, as the configKey
  is. Keyed by the requested name, a baseline judged as "haiku" and a run
  judged as "claude-haiku-5-5" shared a key but no column, and a drop
  from 0.95 to 0.40 read "inconclusive", exit 0. A judge with no
  interval on one side is now "not comparable", never "overlap".
- SupportsTemperature is model-aware: on the API, Claude models from the
  4.7 generation on take no temperature, so the 5.5 judges record null
  with a note instead of sending 0 (a 400). The Sonnet 4.5 bridge judge
  keeps temperature 0.
- claudeCodeVersion leaves the configKey (the CLI updates itself); the
  baseline lists every version seen and --compare reports a change. The
  output names the metrics that flagged a verdict (regressedMetrics,
  improvedMetrics), and only for a comparison actually made.
- eval.sh: its env -u list is tested against blockedCLIEnv; --dry-run
  asks the CLI nothing, --preflight adds the sign-in check; stale
  comments fixed.
- Docs: API-era baselines are not comparable with this instrument on
  any route; the 2026-10-04 CLI baselines are marked as possibly
  affected by the auto-memory leak (#127) and superseded, and the
  readings built on them are flagged unconfirmed.

Co-Authored-By: Claude <noreply@anthropic.com>
@0xMMA
0xMMA merged commit 9e3e741 into main Oct 11, 2026
5 checks passed
@0xMMA
0xMMA deleted the fix/claude-cli-isolation branch October 11, 2026 18:56
0xMMA added a commit that referenced this pull request Oct 11, 2026
Review follow-ups on the two-judge instrument:

- Per-judge columns are keyed by the ID that answered, as the configKey
  is. Keyed by the requested name, a baseline judged as "haiku" and a run
  judged as "claude-haiku-5-5" shared a key but no column, and a drop
  from 0.95 to 0.40 read "inconclusive", exit 0. A judge with no
  interval on one side is now "not comparable", never "overlap".
- SupportsTemperature is model-aware: on the API, Claude models from the
  4.7 generation on take no temperature, so the 5.5 judges record null
  with a note instead of sending 0 (a 400). The Sonnet 4.5 bridge judge
  keeps temperature 0.
- claudeCodeVersion leaves the configKey (the CLI updates itself); the
  baseline lists every version seen and --compare reports a change. The
  output names the metrics that flagged a verdict (regressedMetrics,
  improvedMetrics), and only for a comparison actually made.
- eval.sh: its env -u list is tested against blockedCLIEnv; --dry-run
  asks the CLI nothing, --preflight adds the sign-in check; stale
  comments fixed.
- Docs: API-era baselines are not comparable with this instrument on
  any route; the 2026-10-04 CLI baselines are marked as possibly
  affected by the auto-memory leak (#127) and superseded, and the
  readings built on them are flagged unconfirmed.

Co-Authored-By: Claude <noreply@anthropic.com>
0xMMA added a commit that referenced this pull request Oct 11, 2026
…105)

* chore(eval): run both eval suites through the Claude Code CLI

PR #105 rebased onto main as one commit (the original eleven are on
backup/eval-via-claude-code-pre-rebase). Pipeline and judge run through
the installed Claude Code CLI by default; the judge records temperature
null plus a note; claudeCodeVersion is recorded; .env keys load only
when an API provider is in the run. Claude Code CLI calls run in an
empty KeyLint-owned directory with auto-memory off, and inherited
thinking, output-cap and alias-remapping variables are stripped.

Resolved against main:
- Resolved model IDs: main's Response.Model, answeringModel and
  llm.ModelRecorder replace the PR's ResolvedModels; summary.json keeps
  main's shape (model = resolved ID, modelRequested). The judge records
  its own resolvedModel through a second recorder. UsesAPIKey and
  SupportsTemperature move to internal/llm/traits.go.
- Thinking: main's Request.NoThinking replaces the PR's DisableThinking.
  The CLI keeps ignoring it, as on main, so EVAL_THINKING / --thinking
  are gone; a Fix run records what the service does (fixThinkingLabel).
- Judge: main's noEffort and the PR's nil temperature and deadline.
- Effort, model families, typed errors and the rest of main unchanged.

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

* feat(eval): score every sample with two judges

Haiku 5.5 and Sonnet 5.5 judge each sample in both suites, both through
the Claude Code CLI by default (decision 2026-10-11, E3 phase 1). Full
model IDs: neither has a dated snapshot, and each judge records the ID
that answered.

- summary.json: judges[] (with resolvedModel), avgJudges, judgeCounts;
  results.jsonl: judges / judgeErrors keyed by judge model.
- eval-aggregate.sh: one interval per judge (run and sample level),
  per-judge coverage; nothing averages across judges. Runs from before
  the set (judge / avgJudge) read as a set of one.
- configKey: the judge provider, resolved model and temperature fields
  hold one "+"-joined entry per judge, ordered by ID. A single-judge key
  is unchanged, so every recorded baseline reads "not comparable"
  against a two-judge run instead of crashing.
- --compare: interval rule per metric (deterministic, each judge); any
  metric clearly worse is a regression, an improvement needs one clearly
  better and none worse.
- EVAL_JUDGES=a,b,c chooses the set (bridge run with the old Sonnet 4.5
  judge); EVAL_JUDGE_MODEL is a set of one; both, a hole or a duplicate
  are refused. eval.sh shows the set; go test timeout 10800 s.

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

* docs(eval): two judges in CLAUDE.md and both quality-status files

Instrument note only, no new numbers: the single-judge baselines read
"not comparable" against a two-judge run until new ones are recorded.

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

* fix(eval): judge columns by resolved ID; model-aware judge temperature

Review follow-ups on the two-judge instrument:

- Per-judge columns are keyed by the ID that answered, as the configKey
  is. Keyed by the requested name, a baseline judged as "haiku" and a run
  judged as "claude-haiku-5-5" shared a key but no column, and a drop
  from 0.95 to 0.40 read "inconclusive", exit 0. A judge with no
  interval on one side is now "not comparable", never "overlap".
- SupportsTemperature is model-aware: on the API, Claude models from the
  4.7 generation on take no temperature, so the 5.5 judges record null
  with a note instead of sending 0 (a 400). The Sonnet 4.5 bridge judge
  keeps temperature 0.
- claudeCodeVersion leaves the configKey (the CLI updates itself); the
  baseline lists every version seen and --compare reports a change. The
  output names the metrics that flagged a verdict (regressedMetrics,
  improvedMetrics), and only for a comparison actually made.
- eval.sh: its env -u list is tested against blockedCLIEnv; --dry-run
  asks the CLI nothing, --preflight adds the sign-in check; stale
  comments fixed.
- Docs: API-era baselines are not comparable with this instrument on
  any route; the 2026-10-04 CLI baselines are marked as possibly
  affected by the auto-memory leak (#127) and superseded, and the
  readings built on them are flagged unconfirmed.

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

---------

Co-authored-by: Michael <0xMMA@users.noreply.github.com>
Co-authored-by: Claude <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.

1 participant