DEV Community

J Now
J Now

Posted on

Why skill-tree writes state to two different paths

I built skill-tree to classify my own Claude Code sessions against Anthropic's AI Fluency Index — 11 behaviors measured across 9,830 conversations in their February 2026 study. The core loop is straightforward: analyze session history, assign one of seven archetype cards, pick a behavior I haven't touched as a growth quest for next session.

The growth quest only works if it persists. In Claude Code, that's simple: write to ~/.skill-tree/ and it's there next session.

Cowork broke this immediately.

Cowork's $HOME is ephemeral. Anything written there during a session is gone when the session ends. I didn't catch this in early testing because I was developing against Claude Code exclusively, and the two environments look identical from inside the plugin. The quest would be assigned, written to $HOME/.skill-tree/state.json, and silently vanish before the next SessionStart hook could read it.

The fix required detecting which runtime you're in and routing writes accordingly. Cowork exposes $CLAUDE_PLUGIN_ROOT as a durable path — it survives between sessions. Claude Code doesn't set that variable. So now the state path resolves like this:

const stateDir = process.env.CLAUDE_PLUGIN_ROOT
  ? path.join(process.env.CLAUDE_PLUGIN_ROOT, '.user-state')
  : path.join(os.homedir(), '.skill-tree');
Enter fullscreen mode Exit fullscreen mode


n
Two lines. But it took a full session of debugging a quest that kept resetting to find the right two lines.

The rest of the 7-step pipeline — extract user messages, remote classifier on Fly.io (Claude Haiku), archetype assignment, narrative synthesis, render, return stable URL — runs the same in both environments. The dual state path is the only fork.

If you're building plugins that target both Claude Code and Cowork, test state persistence explicitly in Cowork early. The ephemeral $HOME is not documented prominently, and it will silently swallow anything you write there.

Live example of the rendered archetype card: skill-tree-ai.fly.dev/fixture/illuminator

github.com/robertnowell/skill-tree

Top comments (2)

Collapse
 
a3e_ecosystem profile image
A3E Ecosystem

The silent vanish is the worst version of this bug because there's no error to grep for. We hit a sibling failure: two systems writing the same state object to different stores and slowly drifting because no one wrote a reconciler. Ended up with a state called "all_complete" on one side and "env_prep" on the other for the same artifact. Your routing-by-runtime fix is the right shape; the part we're still figuring out is whether to standardize on one writer (with the other as cache) or keep both and accept the reconciler tax. Did you consider that tradeoff for skill-tree, or was the dual-path fundamentally needed?

Collapse
 
peacebinflow profile image
PEACEBINFLOW

The silent state loss in Cowork is the kind of bug that's almost philosophical. The plugin works perfectly. The code is correct. The file gets written. Then the environment deletes it, and the next session starts with no memory of what came before. It's not a crash. It's not an error. It's just a quiet reset to zero every time, which is somehow worse than a loud failure because you can go days without noticing.

What this points at is a broader compatibility problem that's going to get more common as agent platforms proliferate. Claude Code, Cowork, Cursor, Windsurf—they all look like the same environment from inside a plugin. Same filesystem APIs. Same process model. But they have different assumptions about what "persistent" means. $HOME is only persistent if the runtime decides it is. Environment variables that signal durability are ad-hoc and undocumented. Every plugin author is going to rediscover these differences the hard way, one vanished state file at a time.

The two-line fix is elegant and minimal, but it makes me wonder about the taxonomy of these environment differences. You've got one fork for state persistence. What else might diverge? Network access policies, available binaries, session lifecycle hooks, how tool calls get routed. Each plugin that wants to be cross-platform will end up with its own collection of these tiny detection branches. It's the kind of thing that eventually begs for a shim layer or a standardized environment capability detection mechanism. Do you know if Cowork documents $CLAUDE_PLUGIN_ROOT explicitly somewhere, or did you discover it through experimentation? Either way, that's exactly the kind of knowledge that currently lives in blog posts and GitHub issues rather than in any canonical place.