anthropics / anthropics/claude-code
effortLevel has no session-scoped write channel: only the global ~/.claude/settings.json is live-reread, causing cross-session collisions
- Dominant language
- Python
- Stars
- 145k
- Forks
- 23.1k
- PR merge metrics
- PR metrics pending
Description
## Summary
`effortLevel` has no externally-writable, session-scoped channel: every settings source a hook or `--settings` override can write to eventually funnels back to the single well-known `~/.claude/settings.json` for the *live, per-turn* effort resolution the engine actually applies. This makes it impossible to build an external automation (e.g. a `UserPromptSubmit` hook that auto-classifies effort per prompt) that is safe to run in more than one concurrent `claude` process on the same machine — two sessions in *different, unrelated projects* will silently clobber each other's effort level mid-turn.
## Environment
- CLI: `2.1.270` (`~/.local/bin/claude` → `~/.local/share/claude/versions/2.1.270`)
- VS Code extension: `anthropic.claude-code-2.1.273-darwin-arm64` (bundles its own native binary, separate from the CLI's)
- macOS 23.6.0 (Darwin), arm64
## What we were trying to do
A `UserPromptSubmit` hook classifies each incoming prompt against a rubric (Low/Medium/High/XHigh/Max) and want to set that turn's real reasoning effort accordingly — not just print a label, actually drive `effort.level` (the field Claude Code reports on `PreToolUse`/`PostToolUse`/`Stop`/`SubagentStop` hook payloads). The only externally-reachable lever we found for this is writing the top-level `effortLevel` key to a settings.json file, since:
- `apply_flag_settings` (the real, session-scoped control request `/effort` uses internally) is only reachable from in-process CLI code, never from an external hook subprocess.
- Hook output schemas (checked `UserPromptSubmit`'s `hookSpecificOutput` — `additionalContext`/`sessionTitle`/`suppressOriginalPrompt`) expose no effort-related field at all.
- `CLAUDE_CODE_EFFORT_LEVEL` env var exists and is explicitly session-scoped ("overrides effort this session"), but it's a static pin read once at process launch — not something an already-running process's hook can change per turn (a subprocess's env mutations don't propagate to its already-running parent).
So writing `effortLevel` into a settings.json file (via a hook) is the only accessible mechanism — and by default that's `~/.claude/settings.json`, a single path shared by every concurrent `claude` process on the machine, regardless of project/worktree. Two unrelated sessions (different repos, different windows) racing to set different momentary levels for their own prompt will overwrite each other's value mid-turn.
## What we tried, to give the hook its own isolated slot
1. **A private `--settings ` overlay per process**, set up via a launcher script that generates a unique temp file per session and exports its path in `CLAUDE_EFFORT_SETTINGS_FILE`, then the hook writes `effortLevel` there instead of the shared file when that env var is present. Verified the env var *does* correctly reach the hook subprocess (inherited from the parent as expected), and the hook's write to the overlay file did land correctly on disk.
2. **Writing to the current project's local settings file** (`/.claude/settings.local.json`) instead, on the theory that `--setting-sources=user,project,local` (already part of the default launch flags in both the CLI and the VS Code extension) means project/local scope is also consulted, and a git-worktree-per-session workflow would give each session its own physically separate copy of that file.
## Repro / test performed
Within one live session, same turn each time (hook declares a level for the turn, then we hand-edit a candidate file to a third, distinct value before the turn's `Stop` event fires), and read back the real applied level from the `Stop` hook payload's `effort.level`:
| Turn | Hook declared (written by hook) | Shared `~/.claude/settings.json` | Candidate file hand-edited to | Real `effort.level` on Stop |
|---|---|---|---|---|
| 1 | `medium` | `high` | `--settings` overlay → `low` | `high` (matched the **shared** file, not the overlay) |
| 2 | `medium` | `high` | project-local `.claude/settings.local.json` → `xhigh` | `high` (matched the **shared** file again, project/local ignored) |
Both times the real applied effort matched the single global `~/.claude/settings.json` value, regardless of what any other settings source said. Neither the `--settings`-supplied overlay nor project/local scope is live-reread for this purpose — only the one hardcoded global path is.
## Impact
Any external automation that wants to give a `claude` session a dynamic, per-turn-adjusted reasoning effort (not just a static launch-time default) is forced to write to the one file every concurrent process on the machine also reads/writes, with no available locking or isolation primitive. Running multiple `claude` sessions at once — e.g. several VS Code extension windows on unrelated repositories, which is an entirely ordinary way to use the product — means any two of them can silently overwrite each other's effort level mid-turn, and there is no way for a hook to prevent it, only detect it after the fact by comparing the hook's own declared value against the real `effort.level` on the next `Stop` payload.
## Request
Either of these would close the gap:
1. Make `--settings`-supplied override files (and/or project/local-scope `settings.json`) live-reread for `effortLevel` the same way the global user-scope file evidently already is, so a per-process overlay can actually isolate a session; or
2. Expose a supported hook-output field (e.g. on `UserPromptSubmit`) that applies an effort-level change through the same internal channel the interactive `/effort` command uses (`apply_flag_settings`), scoped to that session only, with no shared file involved at all.
Happy to provide more detail — this was diagnosed via external, black-box testing (hook behavior + `Stop`-payload `effort.level`) plus reading strings out of the installed binary to understand the schema/behavior, not via source access, so I may be missing internal context that would make one of the above obviously easier or harder than it looks from outside.
---
🤖 Filed with the help of [Claude Code](https://claude.com/claude-code), which is also the tool whose behavior this report is about.
Contributor guide
No contributing guide indexed for this repository
Research direction
No repository files or tests are named. Start by tracing the UserPromptSubmit hook, --settings handling, global settings reread, and the apply_flag_settings path, then compare the Stop payload's effort.level with each settings source. Done means a supported per-session, per-turn effort update works without concurrent sessions overwriting one another.
Written by the indexing model from the issue text.
Assessment
- Domain
- cli, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100