AppState

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>) - agentNameRegistryMap<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

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

Relations

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