Repository navigation
Resolve Claude workspace roots so Context Health can scan them - #273
Sanjay Singh (san360) merged 3 commits into
Conversation
Every Claude workspace is silently dropped from the Context Health page, so the page reports "No workspaces have context instruction files" and an agentic readiness of 0 even when the projects are well configured. resolveWorkspaceRoot routes claude-* ids to resolveClaudeRoot, which expects ~/.claude/projects/<encoded> and reads cwd out of the first .jsonl line. But ws.path is already the project root, exactly as it is for codex-* and opencode-* ids. readdirSync finds no .jsonl there, the helper returns null, and buildWorkspaceHealth discards the workspace. Only Claude is affected: the other CLI harnesses take the existsSync branch and resolve correctly. Use ws.path when it is a real project directory, keeping the session file lookup as a fallback for paths that do still point at a Claude session store. Both branches now reject paths that do not exist. On a real log set this takes Context Health from 1 usable workspace to 7, the overall score from 7/100 to 34/100, agentic readiness from 0 to 30, and correctly reports 4 of 7 workspaces as having instruction files.
Sanjay Singh (san360)
left a comment
There was a problem hiding this comment.
Thank you mostafaseyedan for tracking this down and for such a clear write-up! The before/after table and the explanation of why claude-* ids took a different branch from codex-*/opencode-* made the root cause easy to follow, and the regression test that fails on the old implementation is exactly what we like to see.
What we verified
- Reproduced the bug on current
main: a Claude Code project withCLAUDE.md,AGENTS.mdand a.claude/skills/*/SKILL.mdis shown in Context Health as "Unable to resolve workspace root path", and in the unfiltered view it is dropped entirely. - With the PR merged onto current
main, the same project resolves and its three context files are listed (workspace score goes from 12 to 67). We checked this with real Claude Code sessions in a few small test repos, including a folder name with a space and a non-ASCII character, and Copilot sessions in the same folder are unchanged. - A nice side benefit: on
main, a project that happens to contain a.jsonldata file was resolved through that file's first line and could show another project's context files. With your change it resolves to its own folder. - Your new test fails against
main's implementation (1 failed / 38 passed) and passes with the fix. Typecheck, lint, knip, lockfile-lint, the full unit suite (3673 passed), build and bundle-size all pass locally.
One thing we'd like to adjust before merging
When the project folder recorded in the Claude sessions no longer exists (renamed, moved or deleted), ws.path isn't the encoded session folder: addSession in src/core/parser-harnesses.ts falls back to the Claude store root, ~/.claude/projects, itself. isProjectRoot only recognises .../.claude/projects/<encoded>, so it accepts ~/.claude/projects as a project root. As a result:
- Every Claude workspace whose folder is gone resolves to the same root, so
dedupeWorkspaceHealthmerges them into one tile with all their requests and no config files. With two such projects (60 requests each) next to two live ones, the overall score went from 45 to 15 and the merged tile was the largest one on the map. - These workspaces show "Create a .github/copilot-instructions.md…" suggestions instead of "Unable to resolve workspace root path".
- Review Context Files would build its payload from the file tree of
~/.claude/projects(the encoded project folder names and transcript file names) rather than from a project.
On main these workspaces are skipped, so this is new with the PR. The inline suggestion below treats both ~/.claude/projects and ~/.claude/projects/<encoded> as session stores (by exact folder names rather than a substring match on .claude). With it, the live-project fix is unchanged and the deleted-folder case behaves as it does on main. Here's a test that fails on the current PR and passes with the suggestion (it could go in the existing describe('resolveWorkspaceRoot') block near the top of the test file):
it('returns null when the project folder is gone and ws.path fell back to ~/.claude/projects', () => {
const base = makeTempDir();
const store = path.join(base, '.claude', 'projects');
fs.mkdirSync(path.join(store, '-tmp-deleted-project'), { recursive: true });
expect(resolveWorkspaceRoot('claude--tmp-deleted-project', { id: 'claude--tmp-deleted-project', name: 'deleted-project', path: store })).toBeNull();
});A couple of small optional notes are inline.
Next steps
Once that's in, a maintainer will approve the CI run for this fork (it's waiting for approval, which is normal for a first contribution). Thanks again, this makes Context Health much more useful for Claude Code users!
| function isProjectRoot(p: string): boolean { | ||
| try { | ||
| if (!fs.statSync(p).isDirectory()) return false; | ||
| } catch { | ||
| return false; | ||
| } | ||
| return path.basename(path.dirname(p)) !== 'projects' || !path.dirname(p).includes('.claude'); | ||
| } |
There was a problem hiding this comment.
When the project folder no longer exists, ws.path falls back to ~/.claude/projects itself (addSession in parser-harnesses.ts), not to ~/.claude/projects/<encoded>. This check doesn't match that path, so all such workspaces resolve to the store root and get merged into one tile with no config files. Checking exact folder names for both levels also avoids the substring match on .claude (for example a parent folder named .claude-tools):
| function isProjectRoot(p: string): boolean { | |
| try { | |
| if (!fs.statSync(p).isDirectory()) return false; | |
| } catch { | |
| return false; | |
| } | |
| return path.basename(path.dirname(p)) !== 'projects' || !path.dirname(p).includes('.claude'); | |
| } | |
| function isProjectRoot(p: string): boolean { | |
| try { | |
| if (!fs.statSync(p).isDirectory()) return false; | |
| } catch { | |
| return false; | |
| } | |
| return !isClaudeSessionStore(p); | |
| } | |
| /** `~/.claude/projects` itself, or one `<encoded>` folder inside it. */ | |
| function isClaudeSessionStore(p: string): boolean { | |
| const dir = path.resolve(p); | |
| const isStoreRoot = (d: string) => path.basename(d) === 'projects' && path.basename(path.dirname(d)) === '.claude'; | |
| return isStoreRoot(dir) || isStoreRoot(path.dirname(dir)); | |
| } |
| // `ws.path` is normally the project root already, as it is for the other | ||
| // CLI harnesses. Fall back to reading `cwd` out of a session file for | ||
| // workspaces whose path still points at ~/.claude/projects/<encoded>. |
There was a problem hiding this comment.
Optional: a small wording tweak so the comment matches what the parser produces today (the session's cwd when it exists, otherwise the store root):
| // `ws.path` is normally the project root already, as it is for the other | |
| // CLI harnesses. Fall back to reading `cwd` out of a session file for | |
| // workspaces whose path still points at ~/.claude/projects/<encoded>. | |
| // `ws.path` is the session's cwd when that folder still exists, as for the | |
| // other CLI harnesses; otherwise addSession falls back to ~/.claude/projects. | |
| // Keep the session-file lookup for paths that point into the session store. |
| }); | ||
| }); | ||
|
|
||
| describe('resolveWorkspaceRoot', () => { |
There was a problem hiding this comment.
Optional nit: there's already a describe('resolveWorkspaceRoot') block near the top of this file (with the Codex case). Moving these tests into it and using the file's makeTempDir() helper would keep them together and handle cleanup automatically, without the try/finally blocks.
When a Claude project's folder no longer exists, ws.path falls back to ~/.claude/projects. isProjectRoot accepted it, so every such workspace resolved to the store root and merged into one tile. Treat both the store root and its <encoded> folders as session stores, matching exact folder names. Move the resolveWorkspaceRoot tests into the existing block and use makeTempDir. Suggested in review by @san360. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RLDXsjE1DMDor8E6cSLBpH
|
Thank you so much for the careful review, and for testing with real Claude Code sessions. The deleted-folder case was a great catch. I had not thought about I applied all three suggestions:
|
|
Thanks a lot for the quick update, mostafaseyedan, and for folding in all three suggestions so cleanly! We re-checked the new head (
From our side this looks good to merge once a maintainer approves the CI run for the fork. Thanks again for the fix and the careful follow-through! |
Problem
The Context Health page discards every Claude workspace, then reports conclusions drawn from whatever is left. On a real log set it showed:
Overall Score 7/100,Agentic Readiness 20/1001 Active Workspaces,0/1 With Context FilesNone of that was true. The same machine has
CLAUDE.md/AGENTS.mdin four separate projects.resolveWorkspaceRootinsrc/core/config-health-helpers.tsroutesclaude-*ids toresolveClaudeRoot(ws.path), which expects~/.claude/projects/<encoded>/and parsescwdout of the first.jsonlline. Butws.pathis already the project root — the same thingcodex-*andopencode-*ids get, and those take thefs.existsSyncbranch and resolve fine:readdirSyncfinds no.jsonlin a project root, the helper returnsnull,getWorkspaceAnalysisContextreturnsnull, andbuildWorkspaceHealthdrops the workspace. Nothing is logged, so the page presents a near-empty sample as a finished score.Only Claude is affected.
Fix
Use
ws.pathwhen it is a real project directory, and keepresolveClaudeRootas the fallback for paths that genuinely point at a Claude session store. A smallisProjectRoothelper distinguishes the two and also makes both branches reject paths that no longer exist — previously the codex/opencode branch did anexistsSyncwhile the Claude branch had no equivalent guard.Impact
Measured on the same log set, before → after:
The surviving workspaces now scan correctly, e.g.
reconreportsAGENTS.md, CLAUDE.md, .claude/skills/playwright-cli/SKILL.md, .claude/settings.local.json.Testing
Three unit tests added:
ws.pathalready a project root, the legacy session-store path still resolving viacwd, and a non-existent path returningnull. Verified the first fails against the old implementation (1 failed / 38 passed) and all pass with the fix (39 passed).npm run typecheckandnpm run lintclean (0 errors); 62 config-health and analyzer-config tests pass.