Skip to content

feat(claude): support multiple config dirs#227

Closed
ozymandiashh wants to merge 3 commits into
getagentseal:mainfrom
ozymandiashh:feat/multiple-claude-dirs
Closed

feat(claude): support multiple config dirs#227
ozymandiashh wants to merge 3 commits into
getagentseal:mainfrom
ozymandiashh:feat/multiple-claude-dirs

Conversation

@ozymandiashh
Copy link
Copy Markdown
Contributor

Summary

  • add CLAUDE_CONFIG_DIRS support for tracking multiple Claude Code config directories
  • keep existing CLAUDE_CONFIG_DIR and default ~/.claude behavior as fallback
  • dedupe normalized config/project paths and keep Claude Desktop discovery intact
  • document the new env var and add an Unreleased changelog entry

Closes #208.

Validation

  • npx tsc --noEmit
  • npx vitest run tests/providers/claude.test.ts tests/security/prototype-pollution.test.ts tests/provider-registry.test.ts
  • npx vitest run
  • npm run build

@ozymandiashh ozymandiashh marked this pull request as ready for review May 5, 2026 10:17
@iamtoruk
Copy link
Copy Markdown
Member

iamtoruk commented May 6, 2026

Reviewed end-to-end: pulled the branch, type-check + full vitest suite (483 tests pass), then ran 8 adversarial probes against createClaudeProvider. Code is clean and the factory refactor is a nice testability win. Notes below.

Probe results

Case Behavior Verdict
Same dir listed twice Deduped via resolve()
Trailing/double delimiters (:work::personal:) Empty entries skipped
Whitespace-only env value Falls back to CLAUDE_CONFIG_DIR
Empty string env value Falls back to CLAUDE_CONFIG_DIR
Mix valid + nonexistent dirs Nonexistent silently skipped (readdir throws → caught)
Same project name across dirs Merged into one ProjectSummary by the parser ⚠ see #1
Relative paths (./work) Resolved against CWD
Tilde paths (~/.claude-foo) Not expanded — stays literal, silent failure ⚠ see #2

Issues to address before merge

1. Aggregation vs attribution mismatch with the linked issue

The issue asks for tracking multiple accounts. The PR delivers aggregation across multiple dirs. parser.ts:652-661 merges ProjectSummary entries by p.project name — so a project that exists in both ~/.claude-work and ~/.claude-personal collapses into one dashboard row with combined totals.

That may or may not be what @ccrvlh wants. I posted the question on #208 — pausing the merge until they clarify. If aggregation is fine, we ship as-is plus a note. If they need per-account splits, the data model needs a configDir / account label on SessionSource now rather than as a follow-up.

2. Tilde ~/ not expanded — silent failure

resolve() does not expand ~. CLAUDE_CONFIG_DIRS=~/.claude:~/.claude-work resolves to <cwd>/~/.claude (etc.), neither valid, falls through to default. No warning. Two options:

  • Expand: if (trimmed.startsWith('~/')) trimmed = join(homedir(), trimmed.slice(2)) in normalizeDirs.
  • Document: explicitly note in README that tilde isn't supported, require absolute or $HOME-substituted paths.

I'd vote for expansion. Five-line change, no failure mode.

3. Test gap on the merge behavior

The two new tests don't cover the same-project-name-across-dirs case (which is the most likely real-world scenario for a multi-account user). If a future refactor unintentionally splits them by path, no test fails. Worth adding before merge.

Smaller things (non-blocking)

  • Symlinks not resolved. resolve() not realpath() — two symlinks to the same real dir get scanned twice. Per-message dedup keeps correctness; just wasted work.
  • Sequential discovery. for (const claudeDir of …) instead of Promise.all(map(...)). Fine at typical 1–3 dirs.
  • Doc nit. README says "Overrides CLAUDE_CONFIG_DIR when set" — strictly, only when set and containing ≥1 valid entry after trim/dedupe.
  • No CLI flag. Only env var. Probably fine.

Security review

No new attack surface. Env var is user-controlled but its only effect is choosing dirs they read from. readdir/stat only — no execution, no traversal risk into anything sensitive.

Verdict

Hold for #208 reply, then either:

  • Merge as-is + tilde patch + merge-behavior test (if @ccrvlh confirms aggregation is fine).
  • Hold longer and rework the data model for per-account attribution (if they need splits).

I have a worktree set up locally and can apply the tilde fix + the missing test in <10 minutes once we know which path.

@ozymandiashh
Copy link
Copy Markdown
Contributor Author

ozymandiashh commented May 6, 2026

Addressed the hold items from the earlier review:

  • Same project across multiple Claude config dirs now stays split by account via account/accountPath attribution in discovery, parsing, dashboard identity, filters, JSON, and CSV export.
  • Tilde ~/ expansion is supported for CLAUDE_CONFIG_DIR and CLAUDE_CONFIG_DIRS.
  • Added regression coverage for same-project separate accounts, single-entry CLAUDE_CONFIG_DIRS labeling, tilde expansion, duplicate label suffixing, project filters, CSV Account column, and JSON output.
  • Merged latest origin/main and preserved the new canonical cwd grouping per account.

Local verification before push:

  • npm test: passed, 40 files / 543 tests
  • npm run build: passed
  • git diff --check: clean
  • Gemini 3.1 Pro Preview review: PASS
  • Claude Opus 4.7 effort max Argus-style review: PASS CU OBSERVATII, no blockers

@iamtoruk
Copy link
Copy Markdown
Member

@ozymandiashh thanks for the careful work on this — fixture-based tests, clean factory refactor for createClaudeProvider, the ~/ expansion, the normalizeDirs dedup, prototype-safe object construction. All of that is good and worth landing.

I want to flag a design mismatch before we merge, though, because it affects most of the diff. The conversation on #208 settled on option 1: total aggregation across dirs — sessions from ~/.claude-work and ~/.claude-personal that share the same sanitized project path should produce one ProjectSummary row with combined totals. The PR currently implements option 2: per-account attribution — two rows, each tagged work: and personal:. The smoking-gun test at tests/providers/claude.test.ts:200-213 asserts:

expect(projects).toHaveLength(2)
expect(projects.map(p => p.totalApiCalls).sort()).toEqual([1, 1])

For option 1 it should be:

expect(projects).toHaveLength(1)
expect(projects[0].totalApiCalls).toBe(2)

The README change at line 356 also documents the option-2 semantic ("keeps the same sanitized project path separate per account instead of merging usage").

What needs changing

The account / accountPath data model is what we need to drop. Specifically:

  1. src/providers/types.ts:5-6 and src/types.ts:108-109,136-138 — remove the new account / accountPath fields from SessionSource, SessionSummary, ProjectSummary. They have no role under option 1.
  2. src/parser.ts:34-44 — delete projectKey / projectSummaryKey. Revert the map keys in scanProjectDirs and parseProviderSources to project-name-only. Revert mergedMap keying back to p.project.
  3. src/providers/claude.ts — keep the multi-root iteration in discoverSessions, but drop accountLabelForClaudeDir, withClaudeAccounts, and the ClaudeRoot.account/accountPath fields. getClaudeRoots should return a plain string[] of resolved dirs.
  4. src/dashboard.tsx:264-269,289-291,456-475, src/cli.ts:153-154,234-241, src/export.ts:195,212 — revert the account-prefix UI and the CSV Account column.
  5. tests/providers/claude.test.ts:200-213 — flip the merge-across-dirs test to assert length === 1 and combined totals.
  6. Add a test — non-existent dir in CLAUDE_CONFIG_DIRS does not crash and does not block scanning of the real dirs alongside it.
  7. README + CHANGELOG — rewrite the semantic to "sessions across all configured dirs are merged into one row per project", drop CSV Account column docs.

What to keep

  • createClaudeProvider(claudeDirs?, desktopSessionsDir?) factory refactor
  • expandHome, normalizeDirs (with the dedup-on-resolved-path behavior)
  • The fixture-based test infrastructure in tests/providers/claude.test.ts
  • The CLAUDE_CONFIG_DIRS env precedence: CLAUDE_CONFIG_DIRS > CLAUDE_CONFIG_DIR > ~/.claude

After the changes the diff should shrink from ~629 lines to roughly 150-200, mostly concentrated in src/providers/claude.ts and one focused test file.

Why we care about the design

If we ship option 2 today and a user later wants the simpler "I just have multiple dirs, please merge them" behavior, we have to rip out a public-facing field (account shows up in the JSON / CSV / dashboard). Easier to start with option 1 and add attribution later as an opt-in flag if the demand actually materializes.

Sorry for the back-and-forth — the disconnect is on us, the issue thread had ccrvlh confirm option 1 but it wasn't pinned at the top of #208. Happy to mark this as approved for merge once the data-model changes land. Take whatever time you need.

iamtoruk added a commit that referenced this pull request May 10, 2026
…) (#288)

Adds an OS-delimited list env var so a user with more than one
Claude account or profile can scan all of them in a single run.
Sessions across every configured dir merge into one ProjectSummary
per project, matching the option-1 design agreed on the issue
thread (no per-account splitting in the data model or the UI).

Format: `CLAUDE_CONFIG_DIRS=~/.claude-work:~/.claude-personal`
on POSIX, `;`-separated on Windows. Precedence is
CLAUDE_CONFIG_DIRS > CLAUDE_CONFIG_DIR > ~/.claude. Empty entries
in the list are skipped, duplicates are deduped on resolved path,
and a missing or unreadable dir does not abort the scan of the
others. If the user explicitly set CLAUDE_CONFIG_DIRS but every
listed entry is unreadable, a one-line stderr hint identifies the
attempted paths and the platform's expected delimiter, so a
Windows user typing the POSIX `:` does not get a silent zero-row
result. `~` is now also expanded in CLAUDE_CONFIG_DIR for
consistency.

Implementation is intentionally narrow: only `claude.ts` changes,
plus a small parser-cache key update so a stale cache from one
config does not bleed into a run with a different config (matters
for the macOS menubar and GNOME extension which run as long-lived
processes). The merge happens for free in
`src/parser.ts:scanProjectDirs`, which keys ProjectSummary entries
by canonical cwd (or the sanitized slug as a fallback). Two
SessionSource entries with the same `project` field land under the
same key and combine their sessions, regardless of which dir they
came from. No new fields on SessionSource / SessionSummary /
ProjectSummary, and no UI changes.

Tests: 12 fixture-based cases covering the unset path (default
~/.claude), single-dir override via CLAUDE_CONFIG_DIR, multi-dir
override via CLAUDE_CONFIG_DIRS, ~ expansion, dedup of repeated
entries, leading/trailing/doubled delimiters, missing dir
tolerated, file-not-directory entry tolerated, empty
CLAUDE_CONFIG_DIRS falls back to single-dir env, and two
parser-level integration tests asserting (a) two sessions from
two dirs sharing one cwd produce one ProjectSummary with combined
totals and no `account`/`accountPath` fields anywhere, and (b)
two sessions sharing a slug but with different canonical cwds
still merge by slug at the project-rollup layer (option 1
behavior pinned so a future refactor cannot quietly swap to
cwd-aware merging without an explicit opt-in).

Supersedes the alternative implementation in #227, which builds
per-account attribution (option 2) instead.
@iamtoruk
Copy link
Copy Markdown
Member

Closing in favor of #288, which shipped on main today (b72e51e).

@ozymandiashh thanks for the substantial work here — the fixture-based test infrastructure, expandHome, normalizeDirs dedup, and the createClaudeProvider factory refactor were all good ideas, and the design discussion in this PR helped shape what we ended up shipping.

The blocker was scope: this PR went past the option-1 (total aggregation) decision that ccrvlh confirmed in #208 and built option-2 (per-account attribution) on top, which threaded account / accountPath through SessionSource, SessionSummary, ProjectSummary, the dashboard, the CSV export, and the JSON output. Rather than asking you to unwind that, we wrote a narrower replacement (≈300 lines, 1 file in src/) that delivers option 1 only.

If per-account attribution is something you (or other users) want to revisit later, please open a fresh issue and we can spec it as an opt-in flag on top of the current implementation. Sorry for the back-and-forth on this one.

@iamtoruk iamtoruk closed this May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: allow multiple account tracking

2 participants