@edspencer/paddock 0.61.1 → 0.62.0

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.
Files changed (92) hide show
  1. package/README.md +35 -14
  2. package/package.json +1 -1
  3. package/packages/server/dist/app.js +35 -8
  4. package/packages/server/dist/claude-credentials.d.ts +116 -0
  5. package/packages/server/dist/claude-credentials.js +103 -0
  6. package/packages/server/dist/claude-home.d.ts +102 -30
  7. package/packages/server/dist/claude-home.js +437 -97
  8. package/packages/server/dist/claude-instructions.d.ts +121 -0
  9. package/packages/server/dist/claude-instructions.js +114 -0
  10. package/packages/server/dist/claude-mcp.d.ts +146 -0
  11. package/packages/server/dist/claude-mcp.js +367 -0
  12. package/packages/server/dist/claude-settings.d.ts +159 -0
  13. package/packages/server/dist/claude-settings.js +159 -0
  14. package/packages/server/dist/cli/args.d.ts +1 -9
  15. package/packages/server/dist/cli/args.js +36 -18
  16. package/packages/server/dist/cli/paddock.js +20 -39
  17. package/packages/server/dist/config.d.ts +166 -30
  18. package/packages/server/dist/config.js +106 -26
  19. package/packages/server/dist/herdctl-agent-config.d.ts +11 -1
  20. package/packages/server/dist/herdctl-agent-config.js +53 -7
  21. package/packages/server/dist/herdctl-agent-names.d.ts +11 -0
  22. package/packages/server/dist/herdctl-agent-names.js +31 -0
  23. package/packages/server/dist/herdctl.d.ts +69 -13
  24. package/packages/server/dist/herdctl.js +70 -18
  25. package/packages/server/dist/instance-config.js +36 -0
  26. package/packages/server/dist/mcp-servers.d.ts +168 -0
  27. package/packages/server/dist/mcp-servers.js +305 -0
  28. package/packages/server/dist/routes/chats.js +10 -4
  29. package/packages/server/dist/transcripts.d.ts +61 -30
  30. package/packages/server/dist/transcripts.js +141 -46
  31. package/packages/server/package.json +1 -1
  32. package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-Ca905oey.js} +1 -1
  33. package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-ChlC1ezH.js} +1 -1
  34. package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-CslqbhUb.js} +1 -1
  35. package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-DkNdilGo.js} +3 -3
  36. package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-BrlvLc-Y.js} +1 -1
  37. package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-CGqCI5nQ.js} +1 -1
  38. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Cp4KTxE2.js} +1 -1
  39. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-yFxaTv04.js} +1 -1
  40. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-59lhWjRt.js} +1 -1
  41. package/packages/web/dist/assets/channel-CZs6Z1i5.js +2 -0
  42. package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-BrpXGnJg.js} +1 -1
  43. package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-UxBDb5pi.js} +1 -1
  44. package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-D4ZJyUHW.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-DWdPj_8E.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-BIpNbflY.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-f6iJDfdI.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-Bz3TKIJk.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.js → chunk-QZHKN3VN-buQmkafK.js} +1 -1
  50. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +2 -0
  51. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +2 -0
  52. package/packages/web/dist/assets/{core-DgOgLvnM.js → core-TyFI6p0n.js} +1 -1
  53. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-CIkdi0XF.js} +1 -1
  54. package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-BnLJCfNs.js} +1 -1
  55. package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-D0OqfDB9.js} +1 -1
  56. package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DQ2spGrK.js} +1 -1
  57. package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-RDIVQ_S3.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-DKoTfh2C.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-6k0OuSAL.js} +1 -1
  60. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-B-oQaYAC.js} +1 -1
  61. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-B7qpYe9C.js} +1 -1
  62. package/packages/web/dist/assets/{format-BFDqyFuw.js → format-PHdEBNOi.js} +1 -1
  63. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-SfBsqMmx.js} +1 -1
  64. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-CuM4Rl_i.js} +1 -1
  65. package/packages/web/dist/assets/{index-DGCs1YIO.js → index-D_1ogxg1.js} +2 -2
  66. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-Dq-32Oj4.js} +1 -1
  67. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-BLFHCj_o.js} +1 -1
  68. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-CHP-Yu63.js} +1 -1
  69. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-DRuehSqn.js} +1 -1
  70. package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-Bwz2vnH7.js} +1 -1
  71. package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-CeMMnCip.js} +4 -4
  72. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CEdqeoRW.js} +1 -1
  73. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-BdDXBx87.js} +1 -1
  74. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-Bovd1Zhx.js} +1 -1
  75. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-DiyrKnJ2.js} +1 -1
  76. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-BuX7TRVw.js} +1 -1
  77. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-BjetZWJo.js} +1 -1
  78. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.js → stateDiagram-AJRCARHV-4AUBV6dF.js} +1 -1
  79. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.js +2 -0
  80. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-PVr5V_pG.js → timeline-definition-PNZ67QCA-BpQ8g8Bm.js} +1 -1
  81. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-Cj99BWER.js} +1 -1
  82. package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-S7AGTOWw.js} +1 -1
  83. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-LNXkUA7S.js} +1 -1
  84. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.js → xychartDiagram-2RQKCTM6-DGdTh4Xk.js} +1 -1
  85. package/packages/web/dist/index.html +1 -1
  86. package/packages/web/dist/sw.js +1 -1
  87. package/packages/server/dist/cli/claude-home-choice.d.ts +0 -44
  88. package/packages/server/dist/cli/claude-home-choice.js +0 -108
  89. package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
  90. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
  91. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
  92. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +0 -2
package/README.md CHANGED
@@ -26,11 +26,34 @@ claude setup-token # Claude Max/Pro
26
26
  export ANTHROPIC_API_KEY=sk-ant-… # or API billing
27
27
  ```
28
28
 
29
- If you already use Claude Code on this machine, there is nothing to do: with no token
30
- in the environment, Paddock runs against your own `~/.claude` and uses the login
31
- already there — including one held in the **macOS Keychain**, which cannot be shared
32
- any other way. Pass `--isolated-claude-home` to give Paddock a Claude home of its own
33
- instead.
29
+ **If you already use Claude Code on this machine, there is nothing to do.** Paddock
30
+ keeps a Claude home of its own under the data dir, but not a login of its own: it uses
31
+ the one you already have — the macOS Keychain entry on a Mac, your
32
+ `~/.claude/.credentials.json` elsewhere (symlinked in, never copied). Reading a login
33
+ writes nothing, and nothing else is shared with it.
34
+
35
+ To give Paddock its own instead, set `claude: { credentials: own }` in
36
+ `<data-dir>/paddock.config.yaml`; a login is then a token in the environment as above,
37
+ or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. Either way,
38
+ Paddock says at startup if it can find no credentials at all.
39
+
40
+ ## What else Paddock does and does not take from your `~/.claude`
41
+
42
+ Apart from that login: **nothing, by default.** Your `CLAUDE.md`, `agents/`,
43
+ `commands/` and `plugins/` are not loaded, and the hooks your `settings.json` binds to
44
+ tool use do not run. Both are one key in `<data-dir>/paddock.config.yaml`:
45
+
46
+ ```yaml
47
+ claude:
48
+ instructions: host # own | host, default own — CLAUDE.md, agents, commands, plugins
49
+ hooks: host # own | host, default own — shell commands settings.json binds
50
+ ```
51
+
52
+ `instructions: own` is worth knowing about if you have curated a `~/.claude/CLAUDE.md`:
53
+ your Paddock agents will not see it until you set `host`. Each project's own `CLAUDE.md`
54
+ always applies. `hooks` is off by default because inheriting someone's shell commands is
55
+ not a thing to discover after the fact; the rest of your `settings.json` — permissions,
56
+ model, statusline — applies either way.
34
57
 
35
58
  ## Open your own project
36
59
 
@@ -49,13 +72,13 @@ The flag is the consent, so here is everything it does:
49
72
  - creates **`.paddock/`** for the workspace's state and **`.chats/`** for transcripts
50
73
  - appends those two entries to **`.gitignore`**
51
74
 
52
- **Nothing is written into your `~/.claude`, ever** — no file, no symlink, in any
53
- configuration. Sessions found there are *offered* for import: nothing is moved, copied
54
- or linked until you confirm, and your terminal `claude` keeps working exactly as
55
- before. What varies is which Claude home Paddock runs against, and it says which on
56
- startup — with a token in the environment (or `--isolated-claude-home`) it uses its own
57
- under `.paddock/` and relocates transcripts into `.chats/`; without one it uses your
58
- `~/.claude`, and transcripts stay there instead.
75
+ **Nothing is written into your `~/.claude`** — no file, no symlink. Paddock uses a
76
+ Claude home of its own under `.paddock/` and relocates transcripts into `.chats/`.
77
+ Sessions found in `~/.claude` are *offered* for import: nothing is moved, copied or
78
+ linked until you confirm, and your terminal `claude` keeps working exactly as before.
79
+ To share one set of transcripts between Paddock and your terminal instead, set
80
+ `claude: { transcripts: host }` in `<data-dir>/paddock.config.yaml`. (Your *login* is
81
+ already shared, which is why there is nothing to log into — see Credentials above.)
59
82
 
60
83
  To undo it: `rm -rf .paddock .chats` and drop the two `.gitignore` lines.
61
84
 
@@ -70,8 +93,6 @@ Without `--here`, Paddock never touches the directory you ran it from.
70
93
  --here Open the CURRENT directory as the workspace
71
94
  -o, --open Open the app in your browser once it is listening
72
95
  --verbose Show the server's own logs (quiet by default)
73
- --isolated-claude-home
74
- Use Paddock's own Claude home instead of ~/.claude
75
96
  -v, --version Print the version
76
97
  -h, --help Show help
77
98
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edspencer/paddock",
3
- "version": "0.61.1",
3
+ "version": "0.62.0",
4
4
  "description": "Project-first launchpad for Claude Code: server-hosted, persistent, resumable sessions organized by project.",
5
5
  "keywords": [
6
6
  "claude",
@@ -20,6 +20,8 @@ import { promises as fs } from "node:fs";
20
20
  import path from "node:path";
21
21
  import { loadPaddockConfig } from "./config.js";
22
22
  import { ensureClaudeHome, countLegacyTranscriptLinks, findPlantedChatsLinks, } from "./claude-home.js";
23
+ import { loadHostMcpSource } from "./claude-mcp.js";
24
+ import { declaredMcpNotices } from "./mcp-servers.js";
23
25
  import { installHerdctlLogBridge } from "./agent-errors.js";
24
26
  import { ProjectStore, ROOT_KEY } from "./projects.js";
25
27
  import { AttachmentStore } from "./attachments.js";
@@ -108,7 +110,32 @@ export async function buildApp(opts = {}) {
108
110
  // --- project layer + herdctl ------------------------------------------
109
111
  const projects = new ProjectStore(cfg.projectsRoot);
110
112
  await projects.init();
111
- const herdctl = new HerdctlService(cfg);
113
+ // The user's own MCP servers, under `claude.mcpServers: host` (#691 step 5).
114
+ // Read BEFORE the service is constructed because every keeper's agent config
115
+ // carries them, and read exactly once — a `.claude.json` that grows a server
116
+ // mid-run is picked up at the next restart, which is what the notice says.
117
+ // Under `own` (the default) the file is not opened at all.
118
+ const hostMcp = await loadHostMcpSource(cfg);
119
+ for (const notice of hostMcp.notices)
120
+ app.log[notice.level](notice.message);
121
+ // …and the servers this instance declares for ITSELF, from the top-level
122
+ // `mcpServers:` block (#691 step 6). Resolved during config load (it needs
123
+ // `process.env` for the `env:VAR` references), logged here because that is
124
+ // where the logger is. Nothing below carries a value out of the block: the
125
+ // diagnostics name keys and variable names only, and every server mentioned in
126
+ // a notice is rendered by `describeServer`.
127
+ for (const message of cfg.mcpServersDiagnostics.errors)
128
+ app.log.error(message);
129
+ for (const message of cfg.mcpServersDiagnostics.warnings)
130
+ app.log.warn(message);
131
+ for (const notice of declaredMcpNotices({
132
+ servers: cfg.mcpServers,
133
+ hostNames: Object.keys(hostMcp.source.user),
134
+ browserMcp: cfg.browserMcp,
135
+ })) {
136
+ app.log[notice.level](notice.message);
137
+ }
138
+ const herdctl = new HerdctlService(cfg, { ...hostMcp.source, declared: cfg.mcpServers });
112
139
  const git = new GitService(cfg.projectsRoot, cfg.gitAuthor);
113
140
  const githubAuth = new GithubAuth(path.join(cfg.dataDir, "github-auth.json"), cfg.githubClientId);
114
141
  const archive = new ArchiveStore(cfg.dataDir);
@@ -163,13 +190,13 @@ export async function buildApp(opts = {}) {
163
190
  for (const notice of (await ensureClaudeHome(cfg)).notices) {
164
191
  app.log[notice.level](notice.message);
165
192
  }
166
- if (cfg.ownsClaudeHome) {
167
- const stale = await countLegacyTranscriptLinks(cfg.legacyClaudeHome, cfg.dataDir);
168
- if (stale > 0) {
169
- app.log.info(`${stale} transcript symlink(s) from a previous layout still point into ${cfg.dataDir} ` +
170
- `from ${path.join(cfg.legacyClaudeHome, "projects")}. Nothing reads them any more; ` +
171
- `paddock leaves them alone because it does not write to ~/.claude. Safe to delete.`);
172
- }
193
+ // Unconditional since #691 — paddock always owns its home, so there is no
194
+ // longer a layout in which these links are the live ones.
195
+ const stale = await countLegacyTranscriptLinks(cfg.legacyClaudeHome, cfg.dataDir);
196
+ if (stale > 0) {
197
+ app.log.info(`${stale} transcript symlink(s) from a previous layout still point into ${cfg.dataDir} ` +
198
+ `from ${path.join(cfg.legacyClaudeHome, "projects")}. Nothing reads them any more; ` +
199
+ `paddock leaves them alone because it does not write to ~/.claude. Safe to delete.`);
173
200
  }
174
201
  // #682: links an affected build planted at a path the user had not used yet.
175
202
  // Unlike the residue above these are still LIVE — they redirect the user's own
@@ -0,0 +1,116 @@
1
+ /**
2
+ * The `claude.credentials` lever (#691): whose Claude Code LOGIN this instance
3
+ * uses.
4
+ *
5
+ * Paddock always owns its Claude home (`<dataDir>/claude-home`, #691), and that
6
+ * is what made a macOS login invisible (#683). Claude Code files its secure
7
+ * storage entry under a service name derived from the config dir, so the moment
8
+ * paddock points `CLAUDE_CONFIG_DIR` at its own home, `claude /login`'s Keychain
9
+ * entry is under a name paddock's agents never look up. On a Mac with no token in
10
+ * the environment that is an instance which boots cleanly and fails every single
11
+ * turn with "Not logged in".
12
+ *
13
+ * ## The mechanism
14
+ *
15
+ * Read out of the SDK bundle (`@anthropic-ai/claude-agent-sdk/sdk.mjs`), which
16
+ * derives the service name like this:
17
+ *
18
+ * ```js
19
+ * var _z = "-credentials";
20
+ * function yz(e = "") {
21
+ * let t = process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR,
22
+ * r = t !== undefined ? !t : !process.env.CLAUDE_CONFIG_DIR,
23
+ * n = t !== undefined ? t.normalize("NFC") : Yt(),
24
+ * o = r ? "" : `-${sha256(n).digest("hex").substring(0, 8)}`;
25
+ * return `Claude Code${OAUTH_FILE_SUFFIX}${e}${o}`;
26
+ * }
27
+ * ```
28
+ *
29
+ * `OAUTH_FILE_SUFFIX` is `""` in production and the only caller passes `_z`, so a
30
+ * plain `claude /login` files under exactly **`Claude Code-credentials`** — which
31
+ * is the name `probeMacosKeychainLogin` looks for, now confirmed rather than
32
+ * guessed.
33
+ *
34
+ * The load-bearing line is `r`. **`CLAUDE_SECURESTORAGE_CONFIG_DIR`, when
35
+ * DEFINED, overrides `CLAUDE_CONFIG_DIR` for secure storage only.** Define it as
36
+ * the EMPTY STRING and `!t` is true, so the hash suffix is dropped and the
37
+ * plain-name entry — the user's real login — is the one read, while
38
+ * `CLAUDE_CONFIG_DIR` still points at paddock's own home. That decoupling is the
39
+ * whole lever: one variable, no home moved, nothing else shared.
40
+ *
41
+ * The empty string is a value, not a mistake. Anywhere it looks like a bug to
42
+ * "clean up", it is the signal.
43
+ *
44
+ * ## Why this defaults to `host`, against the pattern
45
+ *
46
+ * Every other key in the `claude:` block defaults to `own`, and this one does
47
+ * not. **Isolation is about writes.** Reading a Keychain entry (or a symlinked
48
+ * `~/.claude/.credentials.json`) puts no file of the user's at risk: nothing is
49
+ * created, moved or deleted, and the user's own `claude` is unaffected. What
50
+ * defaulting to `own` WOULD do is recreate #683 — a first run that boots fine,
51
+ * says everything is ready, and fails every turn — which is the worst first
52
+ * impression paddock can make and the one it just made.
53
+ *
54
+ * So the guarantee `own` everywhere buys ("nothing outside the data dir is
55
+ * written") survives this default intact, and the deliberate exception is stated
56
+ * here rather than left to be inferred from a table.
57
+ */
58
+ /**
59
+ * Whose Claude Code login this instance uses — the `claude.credentials` key.
60
+ *
61
+ * `own` = only what is inside paddock's own Claude home: a token in the
62
+ * environment, or a `.credentials.json` an operator put there / logged in with.
63
+ * `host` = this machine's Claude Code login as well — the macOS Keychain entry on
64
+ * darwin, and the user's `~/.claude/.credentials.json` (bridged by symlink) on
65
+ * everything else.
66
+ */
67
+ export type CredentialsMode = "own" | "host";
68
+ /** Sharing a login writes nothing. See the module doc for why this is not `own`. */
69
+ export declare const DEFAULT_CREDENTIALS_MODE: CredentialsMode;
70
+ /** Type guard, so an unknown config value falls back instead of failing a boot. */
71
+ export declare function isKnownCredentialsMode(value: string): value is CredentialsMode;
72
+ /**
73
+ * The variable Claude Code resolves its SECURE-STORAGE scope from, independently
74
+ * of `CLAUDE_CONFIG_DIR`. Not exported by the SDK; read from its bundle.
75
+ */
76
+ export declare const SECURE_STORAGE_DIR_VAR = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
77
+ /**
78
+ * The value that means "use the unsuffixed service name" — i.e. the entry a
79
+ * plain `claude /login` wrote. Empty on purpose; see the module doc.
80
+ */
81
+ export declare const HOST_SECURE_STORAGE_DIR = "";
82
+ /** What {@link applyCredentialsMode} did, for the boot notice. */
83
+ export type CredentialsDecision =
84
+ /** `host`: the variable was set to `""`, so the plain-name login is used. */
85
+ {
86
+ action: "shared";
87
+ }
88
+ /** `own`: the variable is unset, so the login is scoped to paddock's home. */
89
+ | {
90
+ action: "isolated";
91
+ }
92
+ /** An operator set the variable to something of their own; left alone. */
93
+ | {
94
+ action: "deferred";
95
+ value: string;
96
+ };
97
+ /**
98
+ * Put this instance's credentials mode into the environment Claude Code runs in.
99
+ *
100
+ * **Mutates `env`, and by default that is `process.env` — deliberately.** There
101
+ * is no per-invocation seam to use instead: herdctl builds the SDK's `options.env`
102
+ * itself (`withClaudeConfigDir`, which spreads `process.env`) and the CLI runtime
103
+ * lets execa merge over the inherited environment, and neither takes an env
104
+ * contribution from paddock. Unlike the Claude home — which herdctl warns must be
105
+ * per-invocation because one process hosts many agents with potentially different
106
+ * homes — the credentials mode is a property of the INSTANCE: every agent in it
107
+ * has the same answer, so a process-wide value cannot leak one agent's setting
108
+ * into another's. Called once, at boot, before the fleet starts.
109
+ *
110
+ * An operator who set the variable to a non-empty value themselves is honoured
111
+ * rather than clobbered, the same courtesy `CLAUDE_CONFIG_DIR` gets (herdctl#423):
112
+ * they have named a third secure-storage scope that neither mode can express. An
113
+ * EMPTY value is not treated that way — it is exactly what `host` writes, so it is
114
+ * ours by construction and `own` clears it.
115
+ */
116
+ export declare function applyCredentialsMode(mode: CredentialsMode, env?: NodeJS.ProcessEnv): CredentialsDecision;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * The `claude.credentials` lever (#691): whose Claude Code LOGIN this instance
3
+ * uses.
4
+ *
5
+ * Paddock always owns its Claude home (`<dataDir>/claude-home`, #691), and that
6
+ * is what made a macOS login invisible (#683). Claude Code files its secure
7
+ * storage entry under a service name derived from the config dir, so the moment
8
+ * paddock points `CLAUDE_CONFIG_DIR` at its own home, `claude /login`'s Keychain
9
+ * entry is under a name paddock's agents never look up. On a Mac with no token in
10
+ * the environment that is an instance which boots cleanly and fails every single
11
+ * turn with "Not logged in".
12
+ *
13
+ * ## The mechanism
14
+ *
15
+ * Read out of the SDK bundle (`@anthropic-ai/claude-agent-sdk/sdk.mjs`), which
16
+ * derives the service name like this:
17
+ *
18
+ * ```js
19
+ * var _z = "-credentials";
20
+ * function yz(e = "") {
21
+ * let t = process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR,
22
+ * r = t !== undefined ? !t : !process.env.CLAUDE_CONFIG_DIR,
23
+ * n = t !== undefined ? t.normalize("NFC") : Yt(),
24
+ * o = r ? "" : `-${sha256(n).digest("hex").substring(0, 8)}`;
25
+ * return `Claude Code${OAUTH_FILE_SUFFIX}${e}${o}`;
26
+ * }
27
+ * ```
28
+ *
29
+ * `OAUTH_FILE_SUFFIX` is `""` in production and the only caller passes `_z`, so a
30
+ * plain `claude /login` files under exactly **`Claude Code-credentials`** — which
31
+ * is the name `probeMacosKeychainLogin` looks for, now confirmed rather than
32
+ * guessed.
33
+ *
34
+ * The load-bearing line is `r`. **`CLAUDE_SECURESTORAGE_CONFIG_DIR`, when
35
+ * DEFINED, overrides `CLAUDE_CONFIG_DIR` for secure storage only.** Define it as
36
+ * the EMPTY STRING and `!t` is true, so the hash suffix is dropped and the
37
+ * plain-name entry — the user's real login — is the one read, while
38
+ * `CLAUDE_CONFIG_DIR` still points at paddock's own home. That decoupling is the
39
+ * whole lever: one variable, no home moved, nothing else shared.
40
+ *
41
+ * The empty string is a value, not a mistake. Anywhere it looks like a bug to
42
+ * "clean up", it is the signal.
43
+ *
44
+ * ## Why this defaults to `host`, against the pattern
45
+ *
46
+ * Every other key in the `claude:` block defaults to `own`, and this one does
47
+ * not. **Isolation is about writes.** Reading a Keychain entry (or a symlinked
48
+ * `~/.claude/.credentials.json`) puts no file of the user's at risk: nothing is
49
+ * created, moved or deleted, and the user's own `claude` is unaffected. What
50
+ * defaulting to `own` WOULD do is recreate #683 — a first run that boots fine,
51
+ * says everything is ready, and fails every turn — which is the worst first
52
+ * impression paddock can make and the one it just made.
53
+ *
54
+ * So the guarantee `own` everywhere buys ("nothing outside the data dir is
55
+ * written") survives this default intact, and the deliberate exception is stated
56
+ * here rather than left to be inferred from a table.
57
+ */
58
+ /** Sharing a login writes nothing. See the module doc for why this is not `own`. */
59
+ export const DEFAULT_CREDENTIALS_MODE = "host";
60
+ /** Type guard, so an unknown config value falls back instead of failing a boot. */
61
+ export function isKnownCredentialsMode(value) {
62
+ return value === "own" || value === "host";
63
+ }
64
+ /**
65
+ * The variable Claude Code resolves its SECURE-STORAGE scope from, independently
66
+ * of `CLAUDE_CONFIG_DIR`. Not exported by the SDK; read from its bundle.
67
+ */
68
+ export const SECURE_STORAGE_DIR_VAR = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
69
+ /**
70
+ * The value that means "use the unsuffixed service name" — i.e. the entry a
71
+ * plain `claude /login` wrote. Empty on purpose; see the module doc.
72
+ */
73
+ export const HOST_SECURE_STORAGE_DIR = "";
74
+ /**
75
+ * Put this instance's credentials mode into the environment Claude Code runs in.
76
+ *
77
+ * **Mutates `env`, and by default that is `process.env` — deliberately.** There
78
+ * is no per-invocation seam to use instead: herdctl builds the SDK's `options.env`
79
+ * itself (`withClaudeConfigDir`, which spreads `process.env`) and the CLI runtime
80
+ * lets execa merge over the inherited environment, and neither takes an env
81
+ * contribution from paddock. Unlike the Claude home — which herdctl warns must be
82
+ * per-invocation because one process hosts many agents with potentially different
83
+ * homes — the credentials mode is a property of the INSTANCE: every agent in it
84
+ * has the same answer, so a process-wide value cannot leak one agent's setting
85
+ * into another's. Called once, at boot, before the fleet starts.
86
+ *
87
+ * An operator who set the variable to a non-empty value themselves is honoured
88
+ * rather than clobbered, the same courtesy `CLAUDE_CONFIG_DIR` gets (herdctl#423):
89
+ * they have named a third secure-storage scope that neither mode can express. An
90
+ * EMPTY value is not treated that way — it is exactly what `host` writes, so it is
91
+ * ours by construction and `own` clears it.
92
+ */
93
+ export function applyCredentialsMode(mode, env = process.env) {
94
+ const operator = env[SECURE_STORAGE_DIR_VAR];
95
+ if (operator !== undefined && operator !== "")
96
+ return { action: "deferred", value: operator };
97
+ if (mode === "host") {
98
+ env[SECURE_STORAGE_DIR_VAR] = HOST_SECURE_STORAGE_DIR;
99
+ return { action: "shared" };
100
+ }
101
+ delete env[SECURE_STORAGE_DIR_VAR];
102
+ return { action: "isolated" };
103
+ }
@@ -1,26 +1,54 @@
1
1
  import type { PaddockConfig } from "./config.js";
2
2
  /**
3
- * Entries of a Claude home that are USER-LEVEL configuration rather than
4
- * per-instance state, symlinked from the user's home into paddock's own when
5
- * paddock's does not already have its own.
6
- *
7
- * Without this, relocating the home silently changes agent behaviour: the user's
8
- * `~/.claude/CLAUDE.md` is auto-loaded into every session, `settings.json`
9
- * carries permissions and hooks, and `agents/` `commands/` `plugins/` are
10
- * capability. Dropping them is not "a clean home", it is a behaviour regression
11
- * nobody asked for.
12
- *
13
- * `.credentials.json` is the one that matters most: on Linux (and in the Docker
14
- * image) it is where `claude login` puts the OAuth token, so without the bridge
15
- * every file-authenticated install would come back logged out. Symlinked rather
16
- * than copied — a copy would duplicate a live secret onto disk AND diverge the
17
- * moment Claude Code refreshed the token.
18
- *
19
- * Deliberately NOT bridged: `projects/` (that's the whole point), and everything
20
- * that is per-instance runtime state — `todos/`, `shell-snapshots/`, `statsig/`,
21
- * `sessions/`. Those are exactly what should now be per-instance.
3
+ * The user's file-based Claude Code login, bridged in only under
4
+ * `claude.credentials: host` (#691).
5
+ *
6
+ * This is what the lever means on Linux and in the Docker image, where `claude
7
+ * login` writes a file rather than using a keychain: on darwin `credentials: host`
8
+ * is the `CLAUDE_SECURESTORAGE_CONFIG_DIR` empty-string trick, here it is this
9
+ * symlink, and one config key has to mean the same thing on both.
10
+ *
11
+ * Symlinked rather than copied — a copy would duplicate a live secret onto disk
12
+ * AND diverge the moment Claude Code refreshed the token.
13
+ *
14
+ * It was bridged unconditionally before the lever existed, which is why
15
+ * `credentials` defaulting to `host` keeps every existing install working
16
+ * unchanged; `own` is the new behaviour, and it has to also REMOVE a link a
17
+ * previous boot planted, or "isolated" would be a claim that only holds for
18
+ * instances that were never anything else.
19
+ */
20
+ export declare const CREDENTIAL_ENTRY = ".credentials.json";
21
+ /**
22
+ * Entries of the user's Claude home that paddock can bridge into its own — the
23
+ * complete list, for callers that want to reason about the surface rather than
24
+ * about one lever.
25
+ *
26
+ * There is no longer an "always bridged" set: since #691 step 4, every entry here
27
+ * belongs to exactly one key of the `claude:` block, and the mapping is the whole
28
+ * point of the block.
29
+ *
30
+ * | Entry | Lever |
31
+ * |---|---|
32
+ * | `CLAUDE.md`, `agents/`, `commands/`, `plugins/` | `claude.instructions` |
33
+ * | `settings.json` (its `hooks` key) | `claude.hooks` |
34
+ * | `.credentials.json` | `claude.credentials` |
35
+ *
36
+ * `settings.json` appears under `hooks` because that is the half of it a lever
37
+ * governs; its OTHER keys — permissions, model, statusline, `enabledPlugins` —
38
+ * are inherited in every mode, which is why `hooks: own` writes a filtered file
39
+ * rather than declining to bridge one. See `claude-settings.ts`.
40
+ *
41
+ * Deliberately NOT bridged in any mode: `projects/` (that's the whole point), and
42
+ * everything that is per-instance runtime state — `todos/`, `shell-snapshots/`,
43
+ * `statsig/`, `sessions/`. Those are exactly what should be per-instance.
44
+ *
45
+ * The fifth lever, `claude.mcpServers`, has NO row here on purpose: MCP servers
46
+ * are declared in `~/.claude.json`, which is a sibling of the home rather than an
47
+ * entry inside it, so it is not bridgeable at all — paddock reads it and passes
48
+ * the servers programmatically (`claude-mcp.ts`). That asymmetry is the reason
49
+ * MCP inheritance broke separately from everything in this table.
22
50
  */
23
- export declare const BRIDGED_ENTRIES: readonly [".credentials.json", "settings.json", "CLAUDE.md", "agents", "commands", "plugins"];
51
+ export declare const BRIDGEABLE_ENTRIES: readonly string[];
24
52
  /**
25
53
  * What a Keychain probe concluded.
26
54
  *
@@ -42,15 +70,24 @@ export type KeychainProbe = () => Promise<KeychainFinding>;
42
70
  * ## The asymmetry, which is deliberate
43
71
  *
44
72
  * A positive result is trustworthy: an entry under that service name is a login.
45
- * A negative result is NOT — the service name is Claude Code's internal detail
46
- * and can change, and `security` can fail for a dozen reasons that have nothing
47
- * to do with the user. So a miss returns `unknown`, and the caller falls back to
48
- * the existing, correct, generic warning. Guessing the name wrong therefore
49
- * costs nothing beyond the improvement not happening.
73
+ * A negative result is NOT — `security` can fail for a dozen reasons that have
74
+ * nothing to do with the user, and the service name, though now read straight out
75
+ * of the SDK bundle rather than guessed, is still Claude Code's internal detail
76
+ * and can change. So a miss returns `unknown`, and the caller falls back to the
77
+ * existing, correct, generic warning. Being wrong about the name therefore costs
78
+ * nothing beyond the improvement not happening.
50
79
  *
51
80
  * Never prompts and never reads a secret: `find-generic-password` WITHOUT `-w`
52
81
  * reports existence only (attributes, which are discarded), so nothing sensitive
53
82
  * reaches a log, a transcript, or this process's memory.
83
+ *
84
+ * One known imprecision, left in on purpose: Claude Code's own lookup also passes
85
+ * `-a` (`process.env.USER`, falling back to the OS username), and this does not.
86
+ * So an entry filed under a DIFFERENT account still reads as `found` here while
87
+ * Claude Code would miss it — a service running under a different `USER` than the
88
+ * terminal that logged in. Matching that exactly would trade a rare false
89
+ * positive for a rarer false negative, and a false negative is the one that costs
90
+ * something: it downgrades a specific instruction back to the generic warning.
54
91
  */
55
92
  export declare function probeMacosKeychainLogin(platform?: string): Promise<KeychainFinding>;
56
93
  /** A message for the operator, at the level it should be logged at. */
@@ -61,21 +98,48 @@ export interface ClaudeHomeNotice {
61
98
  export interface ClaudeHomeReport {
62
99
  /** Entries symlinked in from the user's home this boot. */
63
100
  bridged: string[];
101
+ /**
102
+ * Entries whose bridge symlink was REMOVED this boot because the lever that
103
+ * governs them now says `own` (#691).
104
+ *
105
+ * The bridge is non-clobbering, so without this an instance that was ever
106
+ * `host` would keep reading the user's file forever after being reconfigured —
107
+ * "isolated" would be a claim that only holds for instances that were never
108
+ * anything else.
109
+ */
110
+ withdrawn: string[];
111
+ /**
112
+ * Entries paddock WROTE into its own home this boot rather than symlinking,
113
+ * because a lever needs part of a file and not the whole of it. Only
114
+ * `settings.json` today — see `claude-settings.ts`.
115
+ */
116
+ generated: string[];
64
117
  /** Messages for `app.ts` to log — config resolution has no logger of its own. */
65
118
  notices: ClaudeHomeNotice[];
66
119
  }
67
120
  /**
68
- * Create paddock's Claude home and bridge the user-level config into it.
121
+ * Create paddock's Claude home and put the user-level config into it that this
122
+ * instance's `claude:` levers say it should have (#691).
69
123
  *
70
- * Idempotent and non-clobbering: an entry paddock's home already has (whether a
71
- * real file the user put there or a symlink from a previous boot) is left
72
- * completely alone, so this is safe to run on every start.
124
+ * Idempotent and non-clobbering: a real file the user put in paddock's home is
125
+ * left completely alone, so this is safe to run on every start. What it DOES
126
+ * maintain is what paddock itself planted — bridge symlinks, withdrawn when a
127
+ * lever flips to `own`, and the generated `settings.json`, regenerated from the
128
+ * user's current one. Ownership of a generated file is decided by hash
129
+ * ({@link classifyOwnEntry}), never by "it is at a name we use".
73
130
  *
74
131
  * Never throws. A home that cannot be created is not a reason to refuse to boot
75
132
  * — Claude Code falls back to its own default and chat still works, which is the
76
133
  * same failure mode `ensureProjectChats` has always had.
134
+ *
135
+ * **Mutates `env`** (`process.env` by default) to carry `claude.credentials` into
136
+ * the environment Claude Code runs in — see {@link applyCredentialsMode} for why
137
+ * that is the only seam available and why it is safe process-wide. Done here
138
+ * because this is already the one call that runs before the fleet starts, and
139
+ * because the credential warning below has to describe the environment it just
140
+ * set rather than the one it found.
77
141
  */
78
- export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "ownsClaudeHome">, env?: NodeJS.ProcessEnv, probeKeychain?: KeychainProbe): Promise<ClaudeHomeReport>;
142
+ export declare function ensureClaudeHome(cfg: Pick<PaddockConfig, "claudeHome" | "legacyClaudeHome" | "claude">, env?: NodeJS.ProcessEnv, probeKeychain?: KeychainProbe): Promise<ClaudeHomeReport>;
79
143
  /** One of the user's transcript folders, made readable through paddock's home. */
80
144
  export interface LegacyMirror {
81
145
  /** Folder name inside `<claudeHome>/projects/` — `encodePathForCli(engineCwd)`. */
@@ -139,6 +203,14 @@ export declare function legacySourcePath(legacyHome: string, folderName: string)
139
203
  * mirroring them would re-offer paddock's own chats as "adoptable". That also
140
204
  * makes the leftovers from before this change inert rather than confusing.
141
205
  *
206
+ * A folder paddock's own home ALREADY links to directly is skipped for the same
207
+ * reason. That is the `claude.transcripts: host` case (#691): there the folder in
208
+ * the user's home is a real directory that paddock itself writes through, so
209
+ * mirroring it would offer every project's own live chats back as importable —
210
+ * the #658 mistake with the arrow reversed. (Adoption de-duplicates sources by
211
+ * resolved real path, so this was noise rather than breakage; it is skipped here
212
+ * because a link paddock plants and then mirrors is confusing to find on disk.)
213
+ *
142
214
  * Idempotent and never throws. Returns every mirror it maintains, not only the
143
215
  * ones planted this call, so a caller can rely on the mapping after any call.
144
216
  */