@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.
- package/README.md +65 -16
- package/package.json +2 -2
- package/packages/server/dist/app.js +48 -8
- package/packages/server/dist/claude-credentials.d.ts +116 -0
- package/packages/server/dist/claude-credentials.js +103 -0
- package/packages/server/dist/claude-home.d.ts +102 -30
- package/packages/server/dist/claude-home.js +437 -97
- package/packages/server/dist/claude-instructions.d.ts +121 -0
- package/packages/server/dist/claude-instructions.js +114 -0
- package/packages/server/dist/claude-mcp.d.ts +158 -0
- package/packages/server/dist/claude-mcp.js +389 -0
- package/packages/server/dist/claude-plugins.d.ts +131 -0
- package/packages/server/dist/claude-plugins.js +394 -0
- package/packages/server/dist/claude-settings.d.ts +159 -0
- package/packages/server/dist/claude-settings.js +159 -0
- package/packages/server/dist/cli/args.d.ts +1 -9
- package/packages/server/dist/cli/args.js +46 -19
- package/packages/server/dist/cli/paddock.js +20 -39
- package/packages/server/dist/config.d.ts +166 -30
- package/packages/server/dist/config.js +106 -26
- package/packages/server/dist/herdctl-agent-config.d.ts +17 -1
- package/packages/server/dist/herdctl-agent-config.js +69 -7
- package/packages/server/dist/herdctl-agent-names.d.ts +11 -0
- package/packages/server/dist/herdctl-agent-names.js +31 -0
- package/packages/server/dist/herdctl.d.ts +75 -13
- package/packages/server/dist/herdctl.js +77 -18
- package/packages/server/dist/instance-config.js +36 -0
- package/packages/server/dist/mcp-servers.d.ts +187 -0
- package/packages/server/dist/mcp-servers.js +399 -0
- package/packages/server/dist/routes/chats.js +10 -4
- package/packages/server/dist/transcripts.d.ts +61 -30
- package/packages/server/dist/transcripts.js +141 -46
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-BgNXqAox.js → InstanceConfigPage-CCk2hVT9.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-DfD_UtqU.js → MarkdownRenderer-B6gST_0U.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-1EXdHXmO.js → ProjectRedirect-BCmKT4mf.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-DHb5vNUp.js → ProjectView-CCN98MWu.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-Br-EhLZE.js → ProjectsGrid-CYzuN_yD.js} +1 -1
- package/packages/web/dist/assets/{arc-C2Jekujs.js → arc-BxvtMCZQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CflCA-em.js → architectureDiagram-3BPJPVTR-Bp6ffJ_u.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Ds7GLNnC.js → blockDiagram-GPEHLZMM-BIY2X8nW.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-D4WyGO6I.js → c4Diagram-AAUBKEIU-Br9stEJS.js} +1 -1
- package/packages/web/dist/assets/channel-CMdgNeFH.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-tQunO_M5.js → chunk-2J33WTMH-Djc7uqMV.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DT5dOWd2.js → chunk-4BX2VUAB-yEVMLksS.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CfTUqUEF.js → chunk-55IACEB6-tJaEdPZg.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-sK5bOAA0.js → chunk-727SXJPM-bWEqh672.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DcMgVe-P.js → chunk-AQP2D5EJ-B8U9fB-T.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-In3h_-7v.js → chunk-FMBD7UC4-BeEITYig.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-DuaIeRaC.js → chunk-ND2GUHAM-L2rpLaoK.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-DeT5mR5T.js → chunk-QZHKN3VN-qqzPMZL_.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU4HSESS.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU4HSESS.js +2 -0
- package/packages/web/dist/assets/{core-DgOgLvnM.js → core-CSaNbmLO.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-CyO-rznJ.js → cose-bilkent-S5V4N54A-BRF7Nech.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-B_9Ihzgn.js → dagre-BM42HDAG-D10rJWOR.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-z5mvZ5YT.js → diagram-2AECGRRQ-DGjT5mAO.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-Bsz0cgfn.js → diagram-5GNKFQAL-DaH3bxHh.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-B29k5f4q.js → diagram-KO2AKTUF-QrLW9Wn6.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-CFrwsHoI.js → diagram-LMA3HP47-BX15wgYN.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-D9Sjc2HR.js → diagram-OG6HWLK6-DXQ_p63s.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-C3vbkD7M.js → erDiagram-TEJ5UH35-C_ioRruE.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-DkMIgntd.js → flowDiagram-I6XJVG4X-7d-xLXLM.js} +1 -1
- package/packages/web/dist/assets/{format-BFDqyFuw.js → format-BSDZezwD.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-nvHanjpW.js → ganttDiagram-6RSMTGT7-DZYx5xcj.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-DbCNfOu7.js → gitGraphDiagram-PVQCEYII-dimo-osm.js} +1 -1
- package/packages/web/dist/assets/{index-DGCs1YIO.js → index-CUnFKMga.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-aJjH9x3D.js → infoDiagram-5YYISTIA-BzApY25x.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DoifwNnZ.js → ishikawaDiagram-YF4QCWOH-DtX47_9p.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Cz9kG9HN.js → journeyDiagram-JHISSGLW-C7fITKg1.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BoXnfr7A.js → kanban-definition-UN3LZRKU-BOrWLf_W.js} +1 -1
- package/packages/web/dist/assets/{linear-BlTTO9yR.js → linear-DuyAaBu6.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-YAG8eSPQ.js → mermaid.core-BQlzuVvO.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DtUBKfkr.js → mindmap-definition-RKZ34NQL-CmP748B5.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-B3Rwpmy0.js → pieDiagram-4H26LBE5-CRd5Pdfq.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B8eJtTyk.js → quadrantDiagram-W4KKPZXB-DpJm0Yw_.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CpAluiY9.js → requirementDiagram-4Y6WPE33-B0o2PRMC.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-Cl_Rd1tN.js → sankeyDiagram-5OEKKPKP-CBi3Ek3v.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-CRpqKSGm.js → sequenceDiagram-3UESZ5HK-C1stxfQg.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-h3h8gXv_.js → stateDiagram-AJRCARHV-lCkiETWK.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-ZgEZRNqp.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-PVr5V_pG.js → timeline-definition-PNZ67QCA-D3DkWJMC.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-De-4NPAX.js → vennDiagram-CIIHVFJN-CO2-yC7K.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-Dtv10D2d.js → wardley-L42UT6IY-Dfjp0kqX.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-p2fJuQ6C.js → wardleyDiagram-YWT4CUSO-CIl0OPwS.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-DyYF-k92.js → xychartDiagram-2RQKCTM6-CuvlQuup.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/server/dist/cli/claude-home-choice.d.ts +0 -44
- package/packages/server/dist/cli/claude-home-choice.js +0 -108
- package/packages/web/dist/assets/channel-B1K_gwAH.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DYQQOKrW.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DYQQOKrW.js +0 -2
- 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
|
|
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
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
// `
|
|
128
|
-
//
|
|
129
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
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
|
|
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"));
|