@edspencer/paddock 0.61.1 → 0.63.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 (94) hide show
  1. package/README.md +65 -16
  2. package/package.json +2 -2
  3. package/packages/server/dist/app.js +48 -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 +158 -0
  11. package/packages/server/dist/claude-mcp.js +389 -0
  12. package/packages/server/dist/claude-plugins.d.ts +131 -0
  13. package/packages/server/dist/claude-plugins.js +394 -0
  14. package/packages/server/dist/claude-settings.d.ts +159 -0
  15. package/packages/server/dist/claude-settings.js +159 -0
  16. package/packages/server/dist/cli/args.d.ts +1 -9
  17. package/packages/server/dist/cli/args.js +46 -19
  18. package/packages/server/dist/cli/paddock.js +20 -39
  19. package/packages/server/dist/config.d.ts +166 -30
  20. package/packages/server/dist/config.js +106 -26
  21. package/packages/server/dist/herdctl-agent-config.d.ts +17 -1
  22. package/packages/server/dist/herdctl-agent-config.js +69 -7
  23. package/packages/server/dist/herdctl-agent-names.d.ts +11 -0
  24. package/packages/server/dist/herdctl-agent-names.js +31 -0
  25. package/packages/server/dist/herdctl.d.ts +75 -13
  26. package/packages/server/dist/herdctl.js +77 -18
  27. package/packages/server/dist/instance-config.js +36 -0
  28. package/packages/server/dist/mcp-servers.d.ts +187 -0
  29. package/packages/server/dist/mcp-servers.js +399 -0
  30. package/packages/server/dist/routes/chats.js +10 -4
  31. package/packages/server/dist/transcripts.d.ts +61 -30
  32. package/packages/server/dist/transcripts.js +141 -46
  33. package/packages/server/package.json +1 -1
  34. package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
  35. package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-B6gST_0U.js} +1 -1
  36. package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-BCmKT4mf.js} +1 -1
  37. package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-CCN98MWu.js} +3 -3
  38. package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-CYzuN_yD.js} +1 -1
  39. package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-BxvtMCZQ.js} +1 -1
  40. package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
  41. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
  42. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
  43. package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
  44. package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
  45. package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
  46. package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-bWEqh672.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
  52. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
  53. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
  54. package/packages/web/dist/assets/{core-DgOgLvnM.js → core-CSaNbmLO.js} +1 -1
  55. package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
  56. package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
  57. package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
  58. package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
  62. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
  63. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
  64. package/packages/web/dist/assets/{format-BFDqyFuw.js → format-BSDZezwD.js} +1 -1
  65. package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
  66. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
  67. package/packages/web/dist/assets/{index-DGCs1YIO.js → index-CUnFKMga.js} +2 -2
  68. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
  69. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
  70. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
  71. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
  72. package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-DuyAaBu6.js} +1 -1
  73. package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-BQlzuVvO.js} +4 -4
  74. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
  75. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
  76. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
  77. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
  78. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
  79. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
  80. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
  81. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
  82. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-PVr5V_pG.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
  83. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
  84. package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
  85. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
  86. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
  87. package/packages/web/dist/index.html +1 -1
  88. package/packages/web/dist/sw.js +1 -1
  89. package/packages/server/dist/cli/claude-home-choice.d.ts +0 -44
  90. package/packages/server/dist/cli/claude-home-choice.js +0 -108
  91. package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
  92. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
  93. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
  94. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-BKRI9TnH.js +0 -2
@@ -0,0 +1,159 @@
1
+ /**
2
+ * The `claude.hooks` lever (#691): whether this instance runs the host machine's
3
+ * Claude Code hooks.
4
+ *
5
+ * This is the one lever in the `claude:` block that is about code execution
6
+ * rather than data. A hook is a shell command Claude Code runs on `PreToolUse`,
7
+ * `PostToolUse`, `SessionStart` and friends, and since #620 paddock has symlinked
8
+ * the user's `~/.claude/settings.json` into its own home unconditionally — so
9
+ * every hook the user has ever configured fires inside every paddock turn, with
10
+ * no key to turn it off. "Isolate it by just trying paddock" was not true, and
11
+ * this is why.
12
+ *
13
+ * ## Why this cannot be a symlink decision
14
+ *
15
+ * `settings.json` is a mixed bag. It carries `hooks`, and also `permissions`,
16
+ * `model`, `statusLine`, `enabledPlugins`, `env`, `outputStyle` — the things a
17
+ * user configures once and expects everywhere. A symlink is all-or-nothing and
18
+ * the file is not, so `hooks: own` has to leave the rest inherited while dropping
19
+ * one key.
20
+ *
21
+ * So paddock WRITES `<ownHome>/settings.json` itself: parse the user's, drop
22
+ * {@link HOST_ONLY_SETTINGS_KEYS}, serialise the remainder. `hooks: host` is the
23
+ * old symlink, unchanged.
24
+ *
25
+ * ### The staleness this buys, which is real
26
+ *
27
+ * A copy diverges the moment the user edits theirs; a symlink does not — and
28
+ * **that difference is live, not just at the next process start.** Claude Code
29
+ * runs a settings-change watcher (~1s poll + 500ms debounce) that follows the
30
+ * symlink to its real target, clears its whole settings memo and re-applies the
31
+ * result mid-session; verified by mutating a symlink target under a running
32
+ * `query()` and watching `Detected change to … (via symlink target …)` come back.
33
+ * So the honest statement is that a symlink propagates an edit in about a second
34
+ * and the copy propagates it at the next paddock restart. Two things make that
35
+ * an acceptable price rather than a design flaw:
36
+ *
37
+ * 1. **Only users who actually have hooks pay it.** {@link planHostSettings}
38
+ * returns `link` when the user's file has none of the governed keys, which is
39
+ * most files — nothing to filter, so nothing is copied and there is nothing to
40
+ * go stale. The copy exists exactly where filtering is doing work.
41
+ * 2. **It is regenerated on every boot** (`ensureClaudeHome`), so a restart is
42
+ * the whole of the fix, and paddock says so in the boot notice.
43
+ *
44
+ * A file-watcher on the user's file, re-running the filter, would close the gap
45
+ * — and because Claude Code's own watcher fires on content change regardless of
46
+ * who wrote it, paddock re-writing the copy WOULD be picked up live. That is a
47
+ * real option if the cadence ever matters. It is not taken here: it puts a
48
+ * watcher on a path in the user's home, races paddock's own write against the
49
+ * 5s echo-suppression window Claude Code uses for exactly this, and buys a
50
+ * narrower window on a file people edit a handful of times a year.
51
+ *
52
+ * ### Which turns this actually governs today, which is not all of them
53
+ *
54
+ * `<claudeHome>/settings.json` is Claude Code's `userSettings` source, and
55
+ * **herdctl does not load that source on the SDK runtime.** `sdk-adapter.js`
56
+ * defaults `settingSources` to `["project"]` for any agent with a
57
+ * `working_directory` — which is every paddock keeper — so the SDK is invoked
58
+ * with `--setting-sources=project` and never reads this file at all. The CLI
59
+ * runtime passes `--setting-sources` only when an agent declares
60
+ * `setting_sources`, and paddock declares none, so `claude -p` runs on the
61
+ * default `["user","project","local"]` and DOES read it.
62
+ *
63
+ * So the host's hooks execute in the sweeper, in triggers, and in
64
+ * `driveMode: batch` chats — and not, today, in a default SDK chat turn. That
65
+ * makes this lever narrower than #691 assumed, and worth keeping regardless:
66
+ * those paths are real code execution, the asymmetry is a herdctl default rather
67
+ * than a guarantee, and a lever that is correct before the default changes is
68
+ * the only kind worth having. The same caveat applies to the keys it KEEPS —
69
+ * the user's `permissions` and `model` reach the CLI paths and not the SDK ones,
70
+ * which was true before this change too.
71
+ *
72
+ * ## What `own` does NOT stop, and should
73
+ *
74
+ * `hooks` is not the only key in `settings.json` that names a command to run.
75
+ * Read straight out of the SDK's own settings schema
76
+ * (`@anthropic-ai/claude-agent-sdk/sdk.mjs`), these do too:
77
+ *
78
+ * - `apiKeyHelper` — "Path to a script that outputs authentication values"
79
+ * - `awsCredentialExport` — "Path to a script that exports AWS credentials"
80
+ * - `awsAuthRefresh` — "Path to a script that refreshes AWS authentication"
81
+ * - `gcpAuthRefresh` — "Command to refresh GCP authentication"
82
+ * - `proxyAuthHelper` — "Shell command that outputs a Proxy-Authorization header"
83
+ * - `otelHeadersHelper` — "Path to a script that outputs OpenTelemetry headers"
84
+ * - `statusLine`, `subagentStatusLine`, `fileSuggestion` — `{type:"command", …}`
85
+ *
86
+ * #691 scopes this lever to `hooks`, so `hooks` is what {@link
87
+ * HOST_ONLY_SETTINGS_KEYS} contains — widening a default-on security lever past
88
+ * its brief is the maintainer's call, not this module's. It is one line when he
89
+ * makes it, and the trade-off is not obvious in either direction: the first five
90
+ * of those are how a user's login WORKS on a corporate setup, so dropping them
91
+ * under a key named `hooks` would break authentication for exactly the people who
92
+ * did nothing wrong — an argument that they belong under `claude.credentials`
93
+ * rather than here. Until someone decides, `hooks: own` means "no host hooks",
94
+ * not "no host commands", and this comment is the difference.
95
+ */
96
+ /**
97
+ * Isolation is the default, and here it is the only defensible one: `host` means
98
+ * arbitrary commands from outside the data dir execute inside paddock's turns,
99
+ * and that is not something a user should opt out of after the fact.
100
+ */
101
+ export const DEFAULT_HOOKS_MODE = "own";
102
+ /** Type guard, so an unknown config value falls back instead of failing a boot. */
103
+ export function isKnownHooksMode(value) {
104
+ return value === "own" || value === "host";
105
+ }
106
+ /** The Claude home entry this lever filters. */
107
+ export const SETTINGS_ENTRY = "settings.json";
108
+ /**
109
+ * Keys of `settings.json` that `hooks: own` drops. See the module doc for the
110
+ * several others that arguably belong here and are deliberately not.
111
+ */
112
+ export const HOST_ONLY_SETTINGS_KEYS = ["hooks"];
113
+ /**
114
+ * Where paddock records the SHA-256 of each file it generated in its own home,
115
+ * so a later boot can tell its own output from something a human put there.
116
+ *
117
+ * A marker key inside `settings.json` would need no second file, but the schema
118
+ * is Claude Code's and adding a key to it is asking for a validation warning on
119
+ * some future version. A hash sidecar is inert by construction.
120
+ */
121
+ export const GENERATED_MARKER_FILE = ".paddock-generated.json";
122
+ /**
123
+ * Decide what paddock's own `settings.json` should be, given the user's.
124
+ *
125
+ * Pure, so the whole decision is testable without a filesystem.
126
+ *
127
+ * The `skip` case is the security-relevant one. A `settings.json` that will not
128
+ * parse cannot be filtered, and the safe response is to plant NOTHING rather than
129
+ * fall back to a symlink — a symlink would hand over the hooks this lever exists
130
+ * to withhold, and it would do it precisely when something is already wrong.
131
+ * Claude Code will not be able to read that file either, so the instance loses
132
+ * nothing it would otherwise have had.
133
+ */
134
+ export function planHostSettings(mode, raw) {
135
+ if (mode === "host")
136
+ return { action: "link" };
137
+ let parsed;
138
+ try {
139
+ parsed = JSON.parse(raw);
140
+ }
141
+ catch (err) {
142
+ return { action: "skip", reason: `it is not valid JSON (${String(err)})` };
143
+ }
144
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
145
+ return { action: "skip", reason: "its top level is not a JSON object" };
146
+ }
147
+ const settings = parsed;
148
+ const dropped = HOST_ONLY_SETTINGS_KEYS.filter((k) => k in settings);
149
+ if (dropped.length === 0)
150
+ return { action: "link" };
151
+ const kept = {};
152
+ // Insertion order preserved, minus the dropped keys — a user comparing the two
153
+ // files should see a deletion, not a reshuffle.
154
+ for (const [k, v] of Object.entries(settings)) {
155
+ if (!dropped.includes(k))
156
+ kept[k] = v;
157
+ }
158
+ return { action: "write", content: `${JSON.stringify(kept, null, 2)}\n`, dropped: [...dropped] };
159
+ }
@@ -32,14 +32,6 @@ export interface CliOptions {
32
32
  verbose: boolean;
33
33
  help: boolean;
34
34
  version: boolean;
35
- /**
36
- * Keep paddock's own Claude home instead of inheriting the user's (#683).
37
- *
38
- * The CLI defaults to running against `~/.claude` when there is no token and
39
- * no explicit home, because that is the only way a macOS Keychain login is
40
- * visible at all. This is the opt-out — see `cli/claude-home-choice.ts`.
41
- */
42
- isolatedClaudeHome: boolean;
43
35
  }
44
36
  /** A usage error. Thrown rather than exiting, so parsing stays testable. */
45
37
  export declare class CliError extends Error {
@@ -61,4 +53,4 @@ export declare function nodeVersionProblem(nodeVersion: string): string | undefi
61
53
  * flag that fixes it.
62
54
  */
63
55
  export declare function explainListenError(err: unknown, host: string, port: string): string;
64
- export declare const USAGE = "paddock \u2014 run a Paddock instance locally\n\nUsage\n npx @edspencer/paddock [options]\n\nOptions\n -p, --port <port> HTTP/WS port (default 4000, or $PORT)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)\n --here Open the CURRENT directory as the workspace (see below)\n -o, --open Open the app in your browser once it is listening\n --verbose Show the server's own logs (quiet by default)\n --isolated-claude-home\n Use Paddock's own Claude home instead of ~/.claude\n (see Credentials below)\n -v, --version Print the Paddock version and exit\n -h, --help Show this help\n\nOpening a directory (--here)\n Run `paddock --here` inside a project and Paddock opens THAT directory as its\n workspace: Claude works in your files, and any Claude Code sessions you already\n have for the directory are offered for import. It:\n\n \u00B7 creates .paddock/ in the directory for this workspace's own state\n \u00B7 creates .chats/ for transcripts, and adds both to .gitignore\n \u00B7 offers your ~/.claude sessions for this directory for import \u2014 nothing\n there is moved, copied or linked until you confirm\n\n Once done, later runs in the same directory resume it \u2014 no flag needed.\n Without --here, Paddock never touches the directory you ran it from.\n\nCredentials\n Paddock drives Claude Code, so it needs Claude credentials. With no\n CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment, it runs\n against your own ~/.claude and uses the Claude Code login already there \u2014\n including one held in the macOS Keychain, which cannot be shared any other\n way. Paddock creates nothing under ~/.claude; your transcripts just stay\n there rather than being relocated into .chats/, and existing sessions are\n still offered for import rather than opened for you.\n\n Pass --isolated-claude-home (or set CLAUDE_HOME) for a Claude home of its\n own; then a login must be a token in the environment, a ~/.claude\n .credentials.json, or `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`.\n With no login at all anywhere, run `claude setup-token`.\n\nYour data\n Everything lives in one directory \u2014 ~/.paddock unless you pass --data-dir.\n Projects, chat transcripts and settings all persist there between runs. Move\n that directory to move your instance; delete it to start over. Nothing is\n stored anywhere else.\n\nNotes\n Binds loopback with authentication disabled, which is safe for a laptop. To\n expose it on a network, set PADDOCK_AUTH_MODE first \u2014 Paddock refuses to bind\n a routable interface wide open. See AUTH.md.\n\nDocs: https://github.com/edspencer/paddock";
56
+ export declare const USAGE = "paddock \u2014 run a Paddock instance locally\n\nUsage\n npx @edspencer/paddock [options]\n\nOptions\n -p, --port <port> HTTP/WS port (default 4000, or $PORT)\n --host <host> Bind address (default 127.0.0.1)\n -d, --data-dir <path> Projects + state (default ~/.paddock, or $PADDOCK_DATA_DIR)\n --here Open the CURRENT directory as the workspace (see below)\n -o, --open Open the app in your browser once it is listening\n --verbose Show the server's own logs (quiet by default)\n -v, --version Print the Paddock version and exit\n -h, --help Show this help\n\nOpening a directory (--here)\n Run `paddock --here` inside a project and Paddock opens THAT directory as its\n workspace: Claude works in your files, and any Claude Code sessions you already\n have for the directory are offered for import. It:\n\n \u00B7 creates .paddock/ in the directory for this workspace's own state\n \u00B7 creates .chats/ for transcripts, and adds both to .gitignore\n \u00B7 offers your ~/.claude sessions for this directory for import \u2014 nothing\n there is moved, copied or linked until you confirm\n\n Once done, later runs in the same directory resume it \u2014 no flag needed.\n Without --here, Paddock never touches the directory you ran it from.\n\nCredentials\n Paddock drives Claude Code, so it needs Claude credentials \u2014 and if you\n already use Claude Code on this machine, it uses the login you already have.\n That is a macOS Keychain entry on a Mac, or your ~/.claude/.credentials.json\n elsewhere (symlinked in, never copied). Reading a login writes nothing.\n\n Otherwise: a CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment,\n or a one-off `CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login`. With no\n login at all anywhere, run `claude setup-token`. Paddock says at startup when\n it can find none.\n\nSharing your Claude Code state\n Apart from that login, Paddock writes nothing outside its data dir by\n default: transcripts go to each project's .chats/, and your ~/.claude is read\n for config only. Each thing it can share is one key in\n <data-dir>/paddock.config.yaml:\n\n claude:\n transcripts: host # own | host, default own\n credentials: host # own | host, default host\n instructions: host # own | host, default own\n hooks: host # own | host, default own\n mcpServers: host # own | host, default own\n\n transcripts: host makes a chat and a `claude --resume` in the same directory\n the same file, live in both directions; deleting such a chat in Paddock\n releases it rather than removing it, because it is your history rather than\n Paddock's copy. credentials: own is the opt-out from sharing the login above.\n\n instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and\n plugins/. Off by default, which is a change: your curated CLAUDE.md does not\n reach Paddock's agents unless you say so. Each project's own CLAUDE.md always\n does.\n\n hooks: host runs the shell commands your ~/.claude/settings.json binds to\n tool use. Off by default \u2014 inheriting someone's hooks is not something to\n discover after the fact. Its other keys (permissions, model, statusline)\n apply either way: under hooks: own Paddock writes its own settings.json\n carrying them with hooks dropped, regenerated at startup.\n\n mcpServers: host attaches the MCP servers declared in your ~/.claude.json \u2014\n the top-level ones, plus a project's own when its directory matches. To give\n this instance a server your machine doesn't have, declare it instead in a\n sibling mcpServers: block of the same file; use env:VAR_NAME anywhere a\n string goes so tokens stay out of it. That keeps a token out of the file, not\n out of `ps`: leave driveMode on its default (session) for a server holding\n one, since batch passes the definition to claude as a command-line argument.\n\nYour data\n Everything lives in one directory \u2014 ~/.paddock unless you pass --data-dir.\n Projects, chat transcripts and settings all persist there between runs. Move\n that directory to move your instance; delete it to start over. Nothing is\n stored anywhere else.\n\nNotes\n Binds loopback with authentication disabled, which is safe for a laptop. To\n expose it on a network, set PADDOCK_AUTH_MODE first \u2014 Paddock refuses to bind\n a routable interface wide open. See AUTH.md.\n\nDocs: https://github.com/edspencer/paddock";
@@ -34,7 +34,6 @@ export function parseArgs(argv) {
34
34
  verbose: false,
35
35
  help: false,
36
36
  version: false,
37
- isolatedClaudeHome: false,
38
37
  };
39
38
  for (let i = 0; i < argv.length; i++) {
40
39
  const arg = argv[i];
@@ -66,9 +65,6 @@ export function parseArgs(argv) {
66
65
  case "--verbose":
67
66
  opts.verbose = true;
68
67
  break;
69
- case "--isolated-claude-home":
70
- opts.isolatedClaudeHome = true;
71
- break;
72
68
  case "-h":
73
69
  case "--help":
74
70
  opts.help = true;
@@ -128,9 +124,6 @@ Options
128
124
  --here Open the CURRENT directory as the workspace (see below)
129
125
  -o, --open Open the app in your browser once it is listening
130
126
  --verbose Show the server's own logs (quiet by default)
131
- --isolated-claude-home
132
- Use Paddock's own Claude home instead of ~/.claude
133
- (see Credentials below)
134
127
  -v, --version Print the Paddock version and exit
135
128
  -h, --help Show this help
136
129
 
@@ -148,18 +141,52 @@ Opening a directory (--here)
148
141
  Without --here, Paddock never touches the directory you ran it from.
149
142
 
150
143
  Credentials
151
- Paddock drives Claude Code, so it needs Claude credentials. With no
152
- CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment, it runs
153
- against your own ~/.claude and uses the Claude Code login already there —
154
- including one held in the macOS Keychain, which cannot be shared any other
155
- way. Paddock creates nothing under ~/.claude; your transcripts just stay
156
- there rather than being relocated into .chats/, and existing sessions are
157
- still offered for import rather than opened for you.
158
-
159
- Pass --isolated-claude-home (or set CLAUDE_HOME) for a Claude home of its
160
- own; then a login must be a token in the environment, a ~/.claude
161
- .credentials.json, or \`CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login\`.
162
- With no login at all anywhere, run \`claude setup-token\`.
144
+ Paddock drives Claude Code, so it needs Claude credentials — and if you
145
+ already use Claude Code on this machine, it uses the login you already have.
146
+ That is a macOS Keychain entry on a Mac, or your ~/.claude/.credentials.json
147
+ elsewhere (symlinked in, never copied). Reading a login writes nothing.
148
+
149
+ Otherwise: a CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY in the environment,
150
+ or a one-off \`CLAUDE_CONFIG_DIR=<data-dir>/claude-home claude login\`. With no
151
+ login at all anywhere, run \`claude setup-token\`. Paddock says at startup when
152
+ it can find none.
153
+
154
+ Sharing your Claude Code state
155
+ Apart from that login, Paddock writes nothing outside its data dir by
156
+ default: transcripts go to each project's .chats/, and your ~/.claude is read
157
+ for config only. Each thing it can share is one key in
158
+ <data-dir>/paddock.config.yaml:
159
+
160
+ claude:
161
+ transcripts: host # own | host, default own
162
+ credentials: host # own | host, default host
163
+ instructions: host # own | host, default own
164
+ hooks: host # own | host, default own
165
+ mcpServers: host # own | host, default own
166
+
167
+ transcripts: host makes a chat and a \`claude --resume\` in the same directory
168
+ the same file, live in both directions; deleting such a chat in Paddock
169
+ releases it rather than removing it, because it is your history rather than
170
+ Paddock's copy. credentials: own is the opt-out from sharing the login above.
171
+
172
+ instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and
173
+ plugins/. Off by default, which is a change: your curated CLAUDE.md does not
174
+ reach Paddock's agents unless you say so. Each project's own CLAUDE.md always
175
+ does.
176
+
177
+ hooks: host runs the shell commands your ~/.claude/settings.json binds to
178
+ tool use. Off by default — inheriting someone's hooks is not something to
179
+ discover after the fact. Its other keys (permissions, model, statusline)
180
+ apply either way: under hooks: own Paddock writes its own settings.json
181
+ carrying them with hooks dropped, regenerated at startup.
182
+
183
+ mcpServers: host attaches the MCP servers declared in your ~/.claude.json —
184
+ the top-level ones, plus a project's own when its directory matches. To give
185
+ this instance a server your machine doesn't have, declare it instead in a
186
+ sibling mcpServers: block of the same file; use env:VAR_NAME anywhere a
187
+ string goes so tokens stay out of it. That keeps a token out of the file, not
188
+ out of \`ps\`: leave driveMode on its default (session) for a server holding
189
+ one, since batch passes the definition to claude as a command-line argument.
163
190
 
164
191
  Your data
165
192
  Everything lives in one directory — ~/.paddock unless you pass --data-dir.
@@ -24,7 +24,6 @@ import path from "node:path";
24
24
  import { spawn } from "node:child_process";
25
25
  import { fileURLToPath } from "node:url";
26
26
  import { HERE_MARKER, isHereWorkspace, countClaudeSessions, ensureGitignored } from "./here.js";
27
- import { chooseClaudeHome, userClaudeHomePath } from "./claude-home-choice.js";
28
27
  import { CliError, USAGE, parseArgs, nodeVersionProblem, explainListenError, } from "./args.js";
29
28
  /** This module's own directory: `<…>/packages/server/dist/cli`. */
30
29
  const moduleDir = path.dirname(fileURLToPath(import.meta.url));
@@ -115,26 +114,19 @@ function noteFirstRun(dataDir) {
115
114
  * effects are announced rather than asked about. Printed only on the first
116
115
  * `--here` in a directory; resuming an already-opened one says nothing.
117
116
  */
118
- function announceHereConsent(dir, dataDir, inheritedHome) {
117
+ function announceHereConsent(dir, dataDir) {
119
118
  console.log([
120
119
  "",
121
120
  ` Opening ${dir} as a Paddock workspace.`,
122
121
  ` · ${path.relative(dir, dataDir) || HERE_MARKER}/ and .chats/ created here, and added to .gitignore`,
123
- // Import consent is UNCHANGED by continuity, which is worth stating
124
- // because it is not what you would predict: reading the user's home does
125
- // not turn their sessions into paddock's chats. Verified against a running
126
- // instance — a pre-existing transcript for the directory comes back from
127
- // `adoptable-chats` (offered) and NOT from `chats` (listed), because
128
- // session discovery is attribution-based, not "whatever is in the folder".
129
- // So #663's confirmation still gates every import.
130
- //
131
- // What continuity DOES change is where transcripts live: with no symlink
132
- // planted in a home paddock does not own (#682), Claude Code keeps writing
133
- // to `~/.claude/projects/<encoded-cwd>/` and `.chats/` stays empty.
134
- inheritedHome === undefined
135
- ? " · ~/.claude sessions for this directory are offered for import (nothing is moved)"
136
- : ` · running against your ${inheritedHome}: sessions there are offered for import ` +
137
- `(nothing is moved), and new transcripts stay there rather than in .chats/`,
122
+ // Worth stating because it is not what you would predict: paddock reading
123
+ // the home a user's sessions live in does not turn them into paddock's
124
+ // chats. Verified against a running instance — a pre-existing transcript
125
+ // for the directory comes back from `adoptable-chats` (offered) and NOT
126
+ // from `chats` (listed), because session discovery is attribution-based,
127
+ // not "whatever is in the folder". So #663's confirmation still gates
128
+ // every import.
129
+ " · ~/.claude sessions for this directory are offered for import (nothing is moved)",
138
130
  " Later runs here resume it — no flag needed.",
139
131
  "",
140
132
  ].join("\n"));
@@ -146,7 +138,9 @@ function announceHereConsent(dir, dataDir, inheritedHome) {
146
138
  * transcripts and prints, and changes nothing on disk.
147
139
  */
148
140
  function offerHereIfSessionsExist(dir) {
149
- const claudeHome = process.env.CLAUDE_HOME ?? path.join(os.homedir(), ".claude");
141
+ // The USER's own home, always: this is looking for `claude` history they made
142
+ // in a terminal, which is by definition in `~/.claude` and never in paddock's.
143
+ const claudeHome = path.join(os.homedir(), ".claude");
150
144
  let sessions = 0;
151
145
  try {
152
146
  sessions = countClaudeSessions(claudeHome, dir);
@@ -219,20 +213,13 @@ async function main() {
219
213
  // is `.git`. A flagless run anywhere else touches nothing.
220
214
  const resuming = !opts.here && isHereWorkspace(cwd);
221
215
  const hereMode = opts.here || resuming;
222
- // Credential continuity (#683). Decided HERE, in the interactive CLI, and
223
- // nowhere else: `resolveClaudeHome`'s precedence is untouched, so a server, a
224
- // container and `node dist/index.js` all keep the owned home. All this does is
225
- // set the same `CLAUDE_HOME` a user could set by hand — the difference is that
226
- // on macOS they currently have no way to know they need to.
227
- const userHome = userClaudeHomePath(os.homedir());
228
- const homeChoice = chooseClaudeHome({
229
- env: process.env,
230
- userClaudeHome: userHome,
231
- userHomeExists: fs.existsSync(userHome),
232
- forceIsolated: opts.isolatedClaudeHome,
233
- });
234
- if (homeChoice.claudeHome !== undefined)
235
- process.env.CLAUDE_HOME = homeChoice.claudeHome;
216
+ // No Claude-home choice happens here any more (#691). The CLI used to point
217
+ // `CLAUDE_HOME` at `~/.claude` so a macOS Keychain login would be visible
218
+ // (#683); paddock now always owns its home, and what a user actually wanted
219
+ // from that — shared transcripts, shared login — are separate `claude:` config
220
+ // keys. `claude.credentials` restores the Keychain half and defaults to `host`,
221
+ // so a Mac whose only login is a `claude /login` still works out of the box
222
+ // with no flag: see `claude-credentials.ts`.
236
223
  // Apply CLI defaults as env vars. This is the whole integration surface: the
237
224
  // server resolves config inside `buildApp()`, so anything set before the
238
225
  // dynamic import below is picked up with no special-casing in config.ts.
@@ -242,7 +229,7 @@ async function main() {
242
229
  (hereMode ? path.join(cwd, HERE_MARKER) : path.join(os.homedir(), ".paddock")));
243
230
  if (hereMode) {
244
231
  if (!resuming)
245
- announceHereConsent(cwd, dataDir, homeChoice.claudeHome);
232
+ announceHereConsent(cwd, dataDir);
246
233
  // The root workspace IS `projectsRoot` (its key is ""), so pointing this at
247
234
  // the user's directory makes that directory the workspace. No schema change,
248
235
  // no new project type — see cli/here.ts.
@@ -308,12 +295,6 @@ async function main() {
308
295
  "",
309
296
  ` Paddock is running at ${url}`,
310
297
  ` Workspace: ${hereMode ? cwd : dataDir}${resuming ? " (resumed)" : ""}`,
311
- // Same rule as the workspace line: running against somebody's own Claude
312
- // home is a real difference in what this instance reads and writes, so it
313
- // is stated every run rather than only on the one that decided it.
314
- ...(homeChoice.claudeHome !== undefined
315
- ? [` Claude home: ${homeChoice.claudeHome} (yours — sessions and login shared)`]
316
- : []),
317
298
  " Press Ctrl-C to stop.",
318
299
  "",
319
300
  ].join("\n"));