AppState
- Entity ID:
ent-20260410-443f33fd538d - Type:
concept - Scope:
shared - Status:
active - Aliases: app state, global state, session state
Description
AppState is the global mutable state object that carries all session-level state in Claude Code. It uses a simple pub-sub store pattern (src/state/store.ts) integrated with React/Ink via useSyncExternalStore. Tools access it through ToolUseContext.getAppState() / setAppState() closures. A separate bootstrap-level STATE object in src/bootstrap/state.ts holds process-wide state that predates the React tree.
Type definition: src/state/AppStateStore.ts (lines 89-452)
What AppState contains
Immutable fields (wrapped in DeepImmutable):
- settings — user settings object
- mainLoopModel — current model selection
- toolPermissionContext — permission mode, tool access rules, deny rules
- UI state: expandedView, footerSelection, statusLineText
- Remote/bridge state: remoteSessionUrl, remoteConnectionStatus
- Agent context: agent, kairosEnabled
Mutable fields (functions, Maps, Sets — no DeepImmutable):
- tasks — task registry by ID (Record<string, TaskState>)
- agentNameRegistry — Map<string, AgentId> for SendMessage routing
- mcp — MCP server connections, tools, commands, resources
- plugins — enabled/disabled plugins, errors, installation status
- notifications — current and queued notifications
- fileHistory — file edit history snapshots
- thinkingEnabled — model thinking toggle
- sessionHooks — hook state map
- teamContext — swarm teammate registry
- inbox — peer messages
Store pattern
src/state/store.ts — minimal pub-sub:
type Store<T> = {
getState: () => T
setState: (updater: (prev: T) => T) => void
subscribe: (listener: Listener) => () => void
}
State updates use immutable updater functions. Listeners fire after changes via Object.is comparison. An optional onChange callback on creation handles side effects.
React/Ink integration
src/state/AppState.tsx provides:
- AppStateProvider — wraps the Ink TUI with the store provider
- useAppState(selector) — memoized selector hook via useSyncExternalStore. Components only re-render when their selected value changes.
- useSetAppState() — returns the store's setState function
- useAppStateStore() — returns the full store object
ToolUseContext bridge
src/Tool.ts (lines 158-227) — ToolUseContext is how tools access state:
type ToolUseContext = {
getAppState(): AppState // Read fresh state
setAppState(f: (prev) => AppState): void // Update state
options: { tools, commands, mcpClients, ... }
abortController: AbortController
readFileState: FileStateCache
// ... 20+ more fields
}
Each tool call receives closures to the store's get/set, ensuring tools always read fresh state and updates propagate to React subscribers.
For sub-agents: setAppState is a no-op (isolation), but setAppStateForTasks routes to the root store so agents can register/kill tasks.
Change observer
src/state/onChangeAppState.ts — single observer that fires on every setAppState:
- Syncs permission mode changes to CCR (web UI) and SDK
- Persists model selection and view state to user settings
- Invalidates API key and credential caches on settings changes
- Re-applies environment variable configuration
Bootstrap STATE vs AppState
src/bootstrap/state.ts (1760 lines) holds process-wide state that exists before the React tree:
- Session ID, parent session ID, original CWD, project root
- Model usage telemetry counters
- API request cache (lastAPIRequest, lastAPIRequestMessages)
- Telemetry providers (OpenTelemetry)
- System prompt section cache
- Plugin state, scheduled tasks state
AppState is session/UI state managed by React. Bootstrap STATE is process-level state managed by getters/setters.
What depends on it
- Every tool — tools read/write AppState via ToolUseContext
- React/Ink TUI — components subscribe to AppState slices via useAppState
- Task system — tasks are stored in
AppState.tasks - Permission system — permission context lives in AppState
- Agent lifecycle — sub-agents get isolated AppState (no-op setAppState)
Design trade-offs
| Decision | Trade-off |
|---|---|
| Single mutable store (not Redux/Zustand) | Simple, minimal overhead, but no middleware, time-travel, or devtools |
| DeepImmutable on some fields, not all | Type safety where possible, but mixed mutability is confusing |
| No-op setAppState for sub-agents | Clean isolation, but agents can't signal state changes to parent |
| Single onChange observer | Centralized side effects, but the observer grows large as features add |
| Bootstrap STATE separate from AppState | Clear lifecycle separation, but two places to look for "global state" |
Key claims
- Simple pub-sub store pattern with React useSyncExternalStore integration
- Tools access state via getAppState/setAppState closures in ToolUseContext
- Sub-agents get isolated state (no-op setAppState, except for task registration)
- Single onChange observer handles all side effects (persistence, sync, cache invalidation)
Relations
rel-appstate-tools: tool-system --[reads/writes]--> AppState (via ToolUseContext)rel-appstate-react: AppState --[drives]--> React/Ink TUI (via useAppState)rel-appstate-tasks: task-system --[stores-in]--> AppState.tasksrel-appstate-bootstrap: bootstrap STATE --[predates]--> AppState
Sources
Source code at src/state/AppStateStore.ts, src/state/store.ts, src/state/AppState.tsx, src/state/onChangeAppState.ts, src/Tool.ts, src/bootstrap/state.ts