Install
$ agentstack add skill-techymt-claude-code-superpowers-state-management ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
State Management
The pattern
Shared mutable state is wrapped in a deep-immutable type. Direct mutation is a compile error. All updates go through a single setter that accepts a pure function (prev: State) => State. Returning the same reference signals "nothing changed" and skips downstream work — no re-render, no diff.
The reducer function is atomic: the entire state object is replaced in one operation. Partial updates are expressed as spreads — the fields that didn't change carry forward from prev; only the changed fields are new values. This makes every transition explicit, traceable, and race-condition-safe.
Why this matters
Claude Code's AppState is a large object — UI flags, permission context, task records, session configuration — shared across tools running concurrently, React components rendering in a terminal, and background task workers. Without an immutability contract, a tool updating tasks could race with a UI component reading toolPermissionContext mid-update, producing torn reads.
The DeepImmutable wrapper makes that impossible at the type level. TypeScript rejects direct assignment to any nested field. The only way to change state is through the reducer, which replaces the entire object atomically. The reference-equality short-circuit (return prev when nothing changed) prevents the scheduler from scheduling re-renders for no-op updates — important when multiple tasks call setAppState in rapid succession.
How to apply it
- Read state via
context.getAppState()— this returns the current immutable snapshot. - Write state via
context.setAppState(prev => next). Always return a new object; never mutateprev. - For nested updates (e.g. a field inside
tasks), spread at every level:{ ...prev, tasks: { ...prev.tasks, [id]: updated } }. - When the update is conditional and the condition is false, return
prevunchanged — this is the reference-equality no-op signal. - For repeated nested updates to the same sub-object, extract a helper that handles the spread and the no-op check — if the updater returns the same reference, return
prevunchanged to avoid triggering subscribers unnecessarily. - Never access state from module-level variables. State always comes from
context.getAppState().
In the source
// Source: src/state/AppStateStore.ts
export type AppState = DeepImmutable
toolPermissionContext: ToolPermissionContext
// ... ~100 more fields
}>
// DeepImmutable makes every field and nested field readonly.
// Direct mutation — state.verbose = true — is a TypeScript error.
// Source: src/utils/task/framework.ts
type SetAppState = (updater: (prev: AppState) => AppState) => void
export function updateTaskState(
taskId: string,
setAppState: SetAppState,
updater: (task: T) => T,
): void {
setAppState(prev => {
const task = prev.tasks?.[taskId] as T | undefined
if (!task) return prev // no-op: task not found
const updated = updater(task)
if (updated === task) return prev // no-op: updater returned same reference
return {
...prev,
tasks: { ...prev.tasks, [taskId]: updated },
}
})
}
// Source: src/tasks/stopTask.ts
setAppState(prev => {
const prevTask = prev.tasks[taskId]
if (!prevTask || prevTask.notified) {
return prev // Return same reference — no re-render triggered
}
return {
...prev,
tasks: {
...prev.tasks,
[taskId]: { ...prevTask, notified: true },
},
}
})
The if (updated === task) return prev check in updateTaskState is subtle but important. Without it, every call to setAppState would trigger subscribers even when nothing changed — in a system where dozens of tasks poll state every second, this would saturate the React render scheduler.
Apply it to your code
Before — mutating state directly and storing it in a module-level variable:
// Wrong: module-level state — shared across calls, causes stale reads
let taskStatuses: Record = {}
async call(args, context) {
taskStatuses[args.taskId] = 'running' // mutation, no broadcast
await doWork(args)
taskStatuses[args.taskId] = 'completed' // UI never sees this
return { data: 'done' }
}
After — atomic reducer updates through context:
async call(args, context) {
// WHY: setAppState broadcasts to all subscribers atomically
context.setAppState(prev => ({
...prev,
tasks: {
...prev.tasks,
[args.taskId]: { ...prev.tasks[args.taskId], status: 'running' },
},
}))
await doWork(args)
// WHY: return prev unchanged when condition not met — skips re-render
context.setAppState(prev => {
const task = prev.tasks[args.taskId]
if (!task || task.status !== 'running') return prev
return {
...prev,
tasks: {
...prev.tasks,
[args.taskId]: { ...task, status: 'completed' },
},
}
})
return { data: 'done' }
}
Signals that you need this pattern
- A tool stores session state in a module-level variable or closure — it will be stale across calls
- A component reads state that was updated by a tool but still shows the old value
- Two concurrent tools updating the same field produce inconsistent results
- State updates inside
call()aren't reflected in the UI until the next full render cycle
Signals that you're over-applying it
- Pure computation inside a tool (local variables, intermediate results) doesn't need
setAppState— only values that need to survive beyond the current call or be visible to other tools or the UI - Don't call
setAppStatein a tight loop for progress updates — batch them or useonProgresscallbacks instead - Don't use AppState for data that belongs in a task's disk-backed output file — large outputs go to disk, not memory
Works with
domain-model— where AppState fits in the overall system modeltask-system— task lifecycle transitions are the most common AppState update patternasync-concurrency— concurrent tools reading and writing state safelytool-definition— howcontext.getAppState()andcontext.setAppState()are accessed fromcall()
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: TechyMT
- Source: TechyMT/claude-code-superpowers
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.