@bordoni/claudep 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.1] - 2026-10-01
10
+
11
+ ### Added
12
+
13
+ - `claudep doctor` warns when your Claude Code asks for approval on every auto-memory write in a profile. Since 2.1.280 a write through the shared `projects/` symlink is judged at `~/.claude/projects`, which auto mode cannot approve ([anthropics/claude-code#98044](https://github.com/anthropics/claude-code/issues/98044)). The README explains it.
14
+
15
+ ### Changed
16
+
17
+ - Caught up with Claude Code 2.1.280, whose list of per-instance files grew. `state/`, `policy-limits.json.stamp.json`, the `.signature.json` files next to `policy-limits.json` and `remote-settings.json`, `daemon.lock`, `shares/`, `storage-v2/` and about twenty-five caches and locks are known-private, so `doctor` stops calling them unclassified.
18
+
19
+ ### Fixed
20
+
21
+ - `loop.md`, which Claude Code keeps per instance, is no longer shared by the rule that links every top-level `*.md`. `claudep doctor` names any `loop.md` symlink an older claudep left in a profile and tells you to remove it.
22
+ - `claudep doctor` on Linux and Windows no longer calls a profile "not logged in yet" when it has no `.credentials.json` but Claude Code reports it logged in. Claude Code can keep the login elsewhere, such as Windows Credential Manager behind a feature flag.
23
+
9
24
  ## [0.4.0] - 2026-09-14
10
25
 
11
26
  ### Added
@@ -86,7 +101,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
86
101
  - Keychain isolation per config dir verified against Claude Code 2.1.259: the service name is `Claude Code-credentials-<sha256(dir)[0:8]>`.
87
102
  - Test suite with `bun test`, a sandboxed `$HOME`, a fake `claude` and `security` on PATH, and a real-shell test for the hook. CI runs typecheck, lint and tests on macOS and Linux.
88
103
 
89
- [Unreleased]: https://github.com/bordoni/claudep/compare/0.4.0...HEAD
104
+ [Unreleased]: https://github.com/bordoni/claudep/compare/0.4.1...HEAD
105
+ [0.4.1]: https://github.com/bordoni/claudep/compare/0.4.0...0.4.1
90
106
  [0.4.0]: https://github.com/bordoni/claudep/compare/0.3.0...0.4.0
91
107
  [0.3.0]: https://github.com/bordoni/claudep/compare/0.2.1...0.3.0
92
108
  [0.2.1]: https://github.com/bordoni/claudep/compare/0.2.0...0.2.1
package/README.md CHANGED
@@ -155,7 +155,11 @@ The shared list is an allowlist, so an account-specific file cannot leak across
155
155
 
156
156
  - The first time you open a repository under a new profile you will re-accept folder trust, and claude.ai connectors need their OAuth redone in that profile.
157
157
  - Two profiles running at the same time write the same `settings.json` and `plugins/`. That is the same situation as two terminals today.
158
- - Background sessions and the daemon are tied to `~/.claude`. As of Claude Code 2.1.263, `claude daemon install` refuses to run with `CLAUDE_CONFIG_DIR` set, and `claude --bg` under a profile runs without the daemon.
158
+ - Background sessions and the daemon are tied to `~/.claude`. As of Claude Code 2.1.280, `claude daemon install` refuses to run with `CLAUDE_CONFIG_DIR` set, and the launcher skips the daemon for `claude --bg` under a profile. Anthropic's docs describe a separate background supervisor per config dir; I have not verified that here.
159
+ - Since Claude Code 2.1.280, every auto-memory write in a profile asks for approval, and auto mode cannot give it. The memory directory sits under the shared `projects/` symlink, and Claude Code now checks a write at the path it lands on, `~/.claude/projects/...`, which it treats as sensitive. This is [anthropics/claude-code#98044](https://github.com/anthropics/claude-code/issues/98044), open upstream. `claudep doctor` warns while your Claude Code is affected. The default profile is not.
160
+ - Skills and plugins synced from claude.ai land in the shared `skills/synced/` and `plugins/synced/`, in one folder per account, so profiles do not mix them. The `syncClaudeAiSkills` and `syncClaudeAiPlugins` switches live in the shared `settings.json` and apply to every profile.
161
+ - Console sign-ins made without an API key live in `~/.config/anthropic`, outside every config dir, so all profiles see the same ones. `ANTHROPIC_PROFILE` picks which one Claude Code uses.
162
+ - In VS Code, `CLAUDE_CONFIG_DIR` in `claudeCode.environmentVariables` must be an absolute path. The session list ignores it when `claudeProcessWrapper` is set.
159
163
  - Set `CLAUDE_PROFILES_DIR` to move the profiles root. Keep it out of iCloud or Dropbox; `.claude.json` is rewritten constantly and sync tools create conflict copies.
160
164
  - Never put `CLAUDE_CONFIG_DIR` in a `settings.json` `env` block. Claude Code detects that mismatch and disables features; `claudep doctor` reports it.
161
165
  - `ANTHROPIC_API_KEY`, `ANTHROPIC_AUTH_TOKEN` and `CLAUDE_CODE_OAUTH_TOKEN` override the login in any config dir, always in `-p` mode. `claudep <name>` and `claudep doctor` warn when one is set and leave it alone.
package/claudep.ts CHANGED
@@ -250,8 +250,9 @@ export const SHARED_DIRS = [
250
250
  "workflows",
251
251
  ] as const;
252
252
 
253
- /** Per-profile state. First block is Claude Code's own runtime-state list;
254
- * the rest are observed extras. Never shared. */
253
+ /** Per-profile state. First block is Claude Code's own runtime-state list as of
254
+ * 2.1.263, then observed extras, then what 2.1.280 added to that list. Never
255
+ * shared, and a name here is never shared by the top-level *.md rule either. */
255
256
  export const KNOWN_PRIVATE = new Set<string>([
256
257
  ".claude.json",
257
258
  ".claude.json.backup",
@@ -301,6 +302,42 @@ export const KNOWN_PRIVATE = new Set<string>([
301
302
  "launch.json",
302
303
  "scheduled_tasks.json",
303
304
  "seed-admin",
305
+ // Added to Claude Code's runtime-state list by 2.1.280 (checked 2026-09-30).
306
+ "state",
307
+ "policy-limits.json.stamp.json",
308
+ "policy-limits.json.signature.json",
309
+ "policy-limits.json.signature-iat.json",
310
+ "remote-settings.json.signature.json",
311
+ "remote-settings.json.signature-iat.json",
312
+ "remote-settings-consent.json",
313
+ "remote-settings-helper-consent",
314
+ "hfi-auth.json",
315
+ "shares",
316
+ "storage-v2",
317
+ "daemon.lock",
318
+ "gh-pr-status-cache.json",
319
+ "active-time.json",
320
+ "server-sessions.json",
321
+ "image-cache",
322
+ "file-transfers",
323
+ "computer-use.lock",
324
+ "server.lock",
325
+ "downloads",
326
+ "scratch",
327
+ "traces",
328
+ "startup-perf",
329
+ "feedback-bundles",
330
+ "ccr",
331
+ "bridge-spawn",
332
+ "local-settings",
333
+ "project-settings",
334
+ "remote",
335
+ "systemd",
336
+ "api-dumps",
337
+ "dump-prompts",
338
+ "antproto.json",
339
+ ".cc-writes",
340
+ "loop.md",
304
341
  ]);
305
342
 
306
343
  /** Keys copied from the base .claude.json into a fresh profile so first-run
@@ -462,7 +499,8 @@ export function sharedItems(base: string): SharedItem[] {
462
499
  for (const f of SHARED_FILES) push(f, "file");
463
500
  if (existsSync(base)) {
464
501
  for (const entry of readdirSync(base, { withFileTypes: true })) {
465
- if (entry.name.endsWith(".md") && !entry.isDirectory()) push(entry.name, "file");
502
+ if (entry.name.endsWith(".md") && !entry.isDirectory() && !KNOWN_PRIVATE.has(entry.name))
503
+ push(entry.name, "file");
466
504
  }
467
505
  }
468
506
  for (const d of SHARED_DIRS) push(d, "dir");
@@ -557,6 +595,18 @@ export function linkState(
557
595
  return existsSync(dest) ? "ok" : "broken";
558
596
  }
559
597
 
598
+ /** Known-private names in a profile that are still symlinks into the base.
599
+ * An older claudep linked every top-level *.md, which once included loop.md;
600
+ * link() never deletes, so doctor names them for the user to remove. */
601
+ export function staleSharedLinks(base: string, dir: string, platform: NodeJS.Platform = process.platform): string[] {
602
+ if (!existsSync(dir)) return [];
603
+ return readdirSync(dir, { withFileTypes: true })
604
+ .filter((e) => e.isSymbolicLink() && KNOWN_PRIVATE.has(e.name))
605
+ .filter((e) => samePath(readlinkSync(join(dir, e.name)), join(base, e.name), platform))
606
+ .map((e) => e.name)
607
+ .sort();
608
+ }
609
+
560
610
  /** What to tell the user when the OS refused to create a symlink. */
561
611
  export function symlinkDeniedHint(platform: NodeJS.Platform, name: string): string {
562
612
  if (platform === "win32")
@@ -598,8 +648,23 @@ export function authEnvOverrides(env: Env = process.env, platform: NodeJS.Platfo
598
648
  * dir. Before it every config dir shared one login. */
599
649
  export const MIN_CLAUDE_VERSION = "2.1.144";
600
650
 
651
+ /** From this Claude Code on, a write through a symlink is also judged at the
652
+ * path it lands on. Auto memory in a profile whose projects/ is shared lands
653
+ * in ~/.claude/projects and asks for approval on every write, which auto mode
654
+ * cannot give (anthropics/claude-code#98044). No fixed version yet. */
655
+ export const MEMORY_SYMLINK_PROMPT_FROM = "2.1.280";
656
+ export const MEMORY_SYMLINK_ISSUE = "https://github.com/anthropics/claude-code/issues/98044";
657
+
601
658
  export type Version = [number, number, number];
602
659
 
660
+ /** True when this Claude Code prompts on every auto-memory write through the
661
+ * shared projects/ symlink. An unknown version is not reported. */
662
+ export function memoryWritesPrompt(v: Version | undefined, shared: readonly SharedItem[]): boolean {
663
+ const from = parseVersion(MEMORY_SYMLINK_PROMPT_FROM);
664
+ if (!v || !from || versionBelow(v, from)) return false;
665
+ return shared.some((s) => s.name === "projects");
666
+ }
667
+
603
668
  /** The first x.y.z in `claude --version` output. */
604
669
  export function parseVersion(text: string): Version | undefined {
605
670
  const m = /(\d+)\.(\d+)\.(\d+)/.exec(text);
@@ -1672,6 +1737,7 @@ function msysConfigDirWarning(L: Layout, env: Env = process.env): string | undef
1672
1737
  async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
1673
1738
  const names = args[0] ? [args[0]] : listProfileNames(L);
1674
1739
  let problems = 0;
1740
+ let v: Version | undefined;
1675
1741
  const launch = findClaude();
1676
1742
  if (launch && launch.kind !== "ps1") {
1677
1743
  const { out } = await captureClaude(L, undefined, ["--version"]);
@@ -1680,7 +1746,7 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
1680
1746
  console.log(
1681
1747
  `${c.dim("·")} claude is the npm cmd shim and runs through cmd.exe; the native installer's claude.exe avoids that hop`,
1682
1748
  );
1683
- const v = parseVersion(out);
1749
+ v = parseVersion(out);
1684
1750
  const floor = parseVersion(MIN_CLAUDE_VERSION);
1685
1751
  if (L.platform === "darwin" && v && floor && versionBelow(v, floor)) {
1686
1752
  bad(
@@ -1715,6 +1781,10 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
1715
1781
  if (unclassified.length) {
1716
1782
  warn(`base items neither shared nor known-private (they stay per-profile): ${unclassified.join(", ")}`);
1717
1783
  }
1784
+ if (memoryWritesPrompt(v, shared))
1785
+ warn(
1786
+ `Claude Code ${v?.join(".")} asks for approval on every auto-memory write in a profile, because projects/ is shared through a symlink; auto mode cannot approve it. Known upstream bug: ${MEMORY_SYMLINK_ISSUE}`,
1787
+ );
1718
1788
 
1719
1789
  for (const name of names) {
1720
1790
  const dir = profileDir(L, name);
@@ -1744,13 +1814,23 @@ async function cmdDoctor(L: Layout, args: string[]): Promise<void> {
1744
1814
  ok(`${shared.length} shared item(s) checked`);
1745
1815
  const strays = readdirSync(dir).filter((n) => !sharedNames.has(n) && !KNOWN_PRIVATE.has(n) && !n.endsWith(".md"));
1746
1816
  if (strays.length) warn(`unexpected private items: ${strays.join(", ")}`);
1817
+ for (const n of staleSharedLinks(L.base, dir, L.platform)) {
1818
+ bad(
1819
+ `${n}: linked to the base by an older claudep, but it is per-profile state now. Remove the symlink: ${join(dir, n)}`,
1820
+ );
1821
+ problems++;
1822
+ }
1823
+ const s = await authStatus(L, dir);
1747
1824
  if (L.platform === "darwin") {
1748
1825
  const svc = keychainService(dir);
1749
1826
  if (await keychainHas(svc)) ok(`keychain item "${svc}" present`);
1750
1827
  else warn(`no keychain item "${svc}". Not logged in yet (claudep ${name} auth login)`);
1751
1828
  } else if (credentialsFileHas(dir)) ok(".credentials.json present");
1829
+ else if (s.loggedIn)
1830
+ console.log(
1831
+ `${c.dim("·")} no .credentials.json in the profile; Claude Code keeps this login elsewhere (on Windows, possibly Credential Manager)`,
1832
+ );
1752
1833
  else warn(`no .credentials.json in the profile. Not logged in yet (claudep ${name} auth login)`);
1753
- const s = await authStatus(L, dir);
1754
1834
  if (s.loggedIn) ok(`logged in as ${s.email ?? "?"} (${s.orgName ?? "?"}, ${s.subscriptionType ?? "?"})`);
1755
1835
  else warn("not logged in");
1756
1836
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bordoni/claudep",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Unofficial, not affiliated with Anthropic. Run Claude Code under separate accounts on one machine (work + personal) with isolated logins and shared config.",
5
5
  "type": "module",
6
6
  "license": "MIT",