@edspencer/paddock 0.61.0 → 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.
- package/README.md +36 -4
- package/package.json +1 -1
- package/packages/server/dist/agent-errors.d.ts +41 -0
- package/packages/server/dist/agent-errors.js +127 -0
- package/packages/server/dist/app.js +57 -9
- 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 +164 -25
- package/packages/server/dist/claude-home.js +528 -77
- 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 +146 -0
- package/packages/server/dist/claude-mcp.js +367 -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 -1
- package/packages/server/dist/cli/args.js +39 -5
- package/packages/server/dist/cli/paddock.js +30 -6
- 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 +11 -1
- package/packages/server/dist/herdctl-agent-config.js +53 -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 +69 -13
- package/packages/server/dist/herdctl.js +70 -18
- package/packages/server/dist/instance-config.js +36 -0
- package/packages/server/dist/mcp-servers.d.ts +168 -0
- package/packages/server/dist/mcp-servers.js +305 -0
- package/packages/server/dist/routes/chats.js +10 -4
- package/packages/server/dist/sweep.js +13 -1
- package/packages/server/dist/transcripts.d.ts +62 -24
- package/packages/server/dist/transcripts.js +141 -28
- package/packages/server/package.json +1 -1
- package/packages/web/dist/assets/{InstanceConfigPage-CzMqTdUU.js → InstanceConfigPage-Ca905oey.js} +1 -1
- package/packages/web/dist/assets/{MarkdownRenderer-YSEZHWDe.js → MarkdownRenderer-ChlC1ezH.js} +1 -1
- package/packages/web/dist/assets/{ProjectRedirect-DLWxZbqK.js → ProjectRedirect-CslqbhUb.js} +1 -1
- package/packages/web/dist/assets/{ProjectView-BFX8ff2n.js → ProjectView-DkNdilGo.js} +3 -3
- package/packages/web/dist/assets/{ProjectsGrid-DLOeY_ww.js → ProjectsGrid-BrlvLc-Y.js} +1 -1
- package/packages/web/dist/assets/{arc-BwC1ik-b.js → arc-CGqCI5nQ.js} +1 -1
- package/packages/web/dist/assets/{architectureDiagram-3BPJPVTR-CsVvXQWw.js → architectureDiagram-3BPJPVTR-Cp4KTxE2.js} +1 -1
- package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-DXQk2bIa.js → blockDiagram-GPEHLZMM-yFxaTv04.js} +1 -1
- package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DXiA26It.js → c4Diagram-AAUBKEIU-59lhWjRt.js} +1 -1
- package/packages/web/dist/assets/channel-CZs6Z1i5.js +2 -0
- package/packages/web/dist/assets/{chunk-2J33WTMH-Dt5nC02A.js → chunk-2J33WTMH-BrpXGnJg.js} +1 -1
- package/packages/web/dist/assets/{chunk-4BX2VUAB-DcjnWvl6.js → chunk-4BX2VUAB-UxBDb5pi.js} +1 -1
- package/packages/web/dist/assets/{chunk-55IACEB6-CRa3LkU4.js → chunk-55IACEB6-D4ZJyUHW.js} +1 -1
- package/packages/web/dist/assets/{chunk-727SXJPM-H3V-11qu.js → chunk-727SXJPM-DWdPj_8E.js} +1 -1
- package/packages/web/dist/assets/{chunk-AQP2D5EJ-DPCrENYV.js → chunk-AQP2D5EJ-BIpNbflY.js} +1 -1
- package/packages/web/dist/assets/{chunk-FMBD7UC4-C4UO0ZTc.js → chunk-FMBD7UC4-f6iJDfdI.js} +1 -1
- package/packages/web/dist/assets/{chunk-ND2GUHAM-BtI37CnI.js → chunk-ND2GUHAM-Bz3TKIJk.js} +1 -1
- package/packages/web/dist/assets/{chunk-QZHKN3VN-C5bTaK5j.js → chunk-QZHKN3VN-buQmkafK.js} +1 -1
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-CrbEUUCd.js +2 -0
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-CrbEUUCd.js +2 -0
- package/packages/web/dist/assets/{core-DB3cpm5N.js → core-TyFI6p0n.js} +1 -1
- package/packages/web/dist/assets/{cose-bilkent-S5V4N54A-q-cjTZO4.js → cose-bilkent-S5V4N54A-CIkdi0XF.js} +1 -1
- package/packages/web/dist/assets/{dagre-BM42HDAG-DYEV5HFo.js → dagre-BM42HDAG-BnLJCfNs.js} +1 -1
- package/packages/web/dist/assets/{diagram-2AECGRRQ-D16XCw78.js → diagram-2AECGRRQ-D0OqfDB9.js} +1 -1
- package/packages/web/dist/assets/{diagram-5GNKFQAL-CeztryYE.js → diagram-5GNKFQAL-DQ2spGrK.js} +1 -1
- package/packages/web/dist/assets/{diagram-KO2AKTUF-D5SLIMDS.js → diagram-KO2AKTUF-RDIVQ_S3.js} +1 -1
- package/packages/web/dist/assets/{diagram-LMA3HP47-Bw-sVPxM.js → diagram-LMA3HP47-DKoTfh2C.js} +1 -1
- package/packages/web/dist/assets/{diagram-OG6HWLK6-DsRMITgx.js → diagram-OG6HWLK6-6k0OuSAL.js} +1 -1
- package/packages/web/dist/assets/{erDiagram-TEJ5UH35-T8Qqc_ku.js → erDiagram-TEJ5UH35-B-oQaYAC.js} +1 -1
- package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-Cyx47UWU.js → flowDiagram-I6XJVG4X-B7qpYe9C.js} +1 -1
- package/packages/web/dist/assets/{format-D2I0duZa.js → format-PHdEBNOi.js} +1 -1
- package/packages/web/dist/assets/{ganttDiagram-6RSMTGT7-DxYJmG9o.js → ganttDiagram-6RSMTGT7-SfBsqMmx.js} +1 -1
- package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-COK5YOTD.js → gitGraphDiagram-PVQCEYII-CuM4Rl_i.js} +1 -1
- package/packages/web/dist/assets/{index-wBXQMkGI.js → index-D_1ogxg1.js} +2 -2
- package/packages/web/dist/assets/{infoDiagram-5YYISTIA-BamRN_Nn.js → infoDiagram-5YYISTIA-Dq-32Oj4.js} +1 -1
- package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-DuOHdpEH.js → ishikawaDiagram-YF4QCWOH-BLFHCj_o.js} +1 -1
- package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-Bri7GKTW.js → journeyDiagram-JHISSGLW-CHP-Yu63.js} +1 -1
- package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-BdkgUnzI.js → kanban-definition-UN3LZRKU-DRuehSqn.js} +1 -1
- package/packages/web/dist/assets/{linear-DOcNPCpB.js → linear-Bwz2vnH7.js} +1 -1
- package/packages/web/dist/assets/{mermaid.core-D9SbVBpN.js → mermaid.core-CeMMnCip.js} +4 -4
- package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-CVmLm7zn.js → mindmap-definition-RKZ34NQL-CEdqeoRW.js} +1 -1
- package/packages/web/dist/assets/{pieDiagram-4H26LBE5-D0N10eVU.js → pieDiagram-4H26LBE5-BdDXBx87.js} +1 -1
- package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-B5gnYdTa.js → quadrantDiagram-W4KKPZXB-Bovd1Zhx.js} +1 -1
- package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-CwtxezNZ.js → requirementDiagram-4Y6WPE33-DiyrKnJ2.js} +1 -1
- package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-D79O3cux.js → sankeyDiagram-5OEKKPKP-BuX7TRVw.js} +1 -1
- package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-xhKcI6tI.js → sequenceDiagram-3UESZ5HK-BjetZWJo.js} +1 -1
- package/packages/web/dist/assets/{stateDiagram-AJRCARHV-DBYMAjcX.js → stateDiagram-AJRCARHV-4AUBV6dF.js} +1 -1
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-Cq84MOtb.js +2 -0
- package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-e-MYF4lm.js → timeline-definition-PNZ67QCA-BpQ8g8Bm.js} +1 -1
- package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-B8UsvRDM.js → vennDiagram-CIIHVFJN-Cj99BWER.js} +1 -1
- package/packages/web/dist/assets/{wardley-L42UT6IY-1ucACjTl.js → wardley-L42UT6IY-S7AGTOWw.js} +1 -1
- package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-DJufLd_u.js → wardleyDiagram-YWT4CUSO-LNXkUA7S.js} +1 -1
- package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-qqaZNlu9.js → xychartDiagram-2RQKCTM6-DGdTh4Xk.js} +1 -1
- package/packages/web/dist/index.html +1 -1
- package/packages/web/dist/sw.js +1 -1
- package/packages/web/dist/assets/channel-DDgTXrGP.js +0 -2
- package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DU1kd_Vu.js +0 -2
- package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DU1kd_Vu.js +0 -2
- package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CZtr9Jvq.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
|
+
}
|
|
@@ -53,4 +53,4 @@ export declare function nodeVersionProblem(nodeVersion: string): string | undefi
|
|
|
53
53
|
* flag that fixes it.
|
|
54
54
|
*/
|
|
55
55
|
export declare function explainListenError(err: unknown, host: string, port: string): string;
|
|
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
|
|
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\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\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";
|
|
@@ -134,16 +134,50 @@ Opening a directory (--here)
|
|
|
134
134
|
|
|
135
135
|
· creates .paddock/ in the directory for this workspace's own state
|
|
136
136
|
· creates .chats/ for transcripts, and adds both to .gitignore
|
|
137
|
-
·
|
|
137
|
+
· offers your ~/.claude sessions for this directory for import — nothing
|
|
138
|
+
there is moved, copied or linked until you confirm
|
|
138
139
|
|
|
139
140
|
Once done, later runs in the same directory resume it — no flag needed.
|
|
140
141
|
Without --here, Paddock never touches the directory you ran it from.
|
|
141
142
|
|
|
142
143
|
Credentials
|
|
143
|
-
Paddock drives Claude Code, so it needs Claude credentials
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
+
|
|
166
|
+
transcripts: host makes a chat and a \`claude --resume\` in the same directory
|
|
167
|
+
the same file, live in both directions; deleting such a chat in Paddock
|
|
168
|
+
releases it rather than removing it, because it is your history rather than
|
|
169
|
+
Paddock's copy. credentials: own is the opt-out from sharing the login above.
|
|
170
|
+
|
|
171
|
+
instructions: host loads your ~/.claude CLAUDE.md, agents/, commands/ and
|
|
172
|
+
plugins/. Off by default, which is a change: your curated CLAUDE.md does not
|
|
173
|
+
reach Paddock's agents unless you say so. Each project's own CLAUDE.md always
|
|
174
|
+
does.
|
|
175
|
+
|
|
176
|
+
hooks: host runs the shell commands your ~/.claude/settings.json binds to
|
|
177
|
+
tool use. Off by default — inheriting someone's hooks is not something to
|
|
178
|
+
discover after the fact. Its other keys (permissions, model, statusline)
|
|
179
|
+
apply either way: under hooks: own Paddock writes its own settings.json
|
|
180
|
+
carrying them with hooks dropped, regenerated at startup.
|
|
147
181
|
|
|
148
182
|
Your data
|
|
149
183
|
Everything lives in one directory — ~/.paddock unless you pass --data-dir.
|
|
@@ -119,6 +119,13 @@ function announceHereConsent(dir, dataDir) {
|
|
|
119
119
|
"",
|
|
120
120
|
` Opening ${dir} as a Paddock workspace.`,
|
|
121
121
|
` · ${path.relative(dir, dataDir) || HERE_MARKER}/ and .chats/ created here, and added to .gitignore`,
|
|
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.
|
|
122
129
|
" · ~/.claude sessions for this directory are offered for import (nothing is moved)",
|
|
123
130
|
" Later runs here resume it — no flag needed.",
|
|
124
131
|
"",
|
|
@@ -131,7 +138,9 @@ function announceHereConsent(dir, dataDir) {
|
|
|
131
138
|
* transcripts and prints, and changes nothing on disk.
|
|
132
139
|
*/
|
|
133
140
|
function offerHereIfSessionsExist(dir) {
|
|
134
|
-
|
|
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");
|
|
135
144
|
let sessions = 0;
|
|
136
145
|
try {
|
|
137
146
|
sessions = countClaudeSessions(claudeHome, dir);
|
|
@@ -204,6 +213,13 @@ async function main() {
|
|
|
204
213
|
// is `.git`. A flagless run anywhere else touches nothing.
|
|
205
214
|
const resuming = !opts.here && isHereWorkspace(cwd);
|
|
206
215
|
const hereMode = opts.here || resuming;
|
|
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`.
|
|
207
223
|
// Apply CLI defaults as env vars. This is the whole integration surface: the
|
|
208
224
|
// server resolves config inside `buildApp()`, so anything set before the
|
|
209
225
|
// dynamic import below is picked up with no special-casing in config.ts.
|
|
@@ -239,19 +255,27 @@ async function main() {
|
|
|
239
255
|
// `[fleet-manager] …` lines via console.info — pino's
|
|
240
256
|
// level cannot reach those.
|
|
241
257
|
// An explicit value for either is left alone, and `--verbose` skips both.
|
|
258
|
+
// A LEVEL is not enough, which is what #684 was about. Background job failures
|
|
259
|
+
// are logged at `error` — level 50, above every threshold either variable can
|
|
260
|
+
// set — and one of them is a bare `console.error` inside the engine. So quiet
|
|
261
|
+
// mode is also stated as a fact the server can act on: `agent-errors.ts` reads
|
|
262
|
+
// this to collapse a recognised, non-fatal failure to its cause instead of
|
|
263
|
+
// printing a stack trace with 2 KB of system prompt in it. Internal to the
|
|
264
|
+
// CLI; `--verbose` leaves it unset and nothing is suppressed.
|
|
242
265
|
if (!opts.verbose) {
|
|
243
266
|
if (process.env.LOG_LEVEL === undefined)
|
|
244
267
|
process.env.LOG_LEVEL = "warn";
|
|
245
268
|
if (process.env.HERDCTL_LOG_LEVEL === undefined)
|
|
246
269
|
process.env.HERDCTL_LOG_LEVEL = "warn";
|
|
270
|
+
process.env.PADDOCK_QUIET = "1";
|
|
247
271
|
}
|
|
248
272
|
// No credential preflight here: `ensureClaudeHome` (claude-home.ts) already
|
|
249
273
|
// warns at boot, and it is the only check that can be right. It tests the home
|
|
250
|
-
// paddock will ACTUALLY use —
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
274
|
+
// paddock will ACTUALLY use — after the bridge has symlinked in any
|
|
275
|
+
// `~/.claude/.credentials.json`, and, on darwin, after probing the Keychain for
|
|
276
|
+
// the login `CLAUDE_CONFIG_DIR` scoping hides (#683). It logs at `warn`, which
|
|
277
|
+
// survives the quiet default set just above. Continuity (chosen above) usually
|
|
278
|
+
// means it has nothing to say, because there is nothing left to bridge.
|
|
255
279
|
const port = process.env.PORT ?? "4000";
|
|
256
280
|
const host = process.env.HOST ?? "127.0.0.1";
|
|
257
281
|
// Imported dynamically, AFTER the env above is set: a static import would
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type DriveMode } from "./models.js";
|
|
2
|
+
import { type TranscriptsMode } from "./transcripts.js";
|
|
3
|
+
import { type CredentialsMode } from "./claude-credentials.js";
|
|
4
|
+
import { type InstructionsMode } from "./claude-instructions.js";
|
|
5
|
+
import { type HooksMode } from "./claude-settings.js";
|
|
6
|
+
import { type McpServersMode, type McpServerDefs } from "./claude-mcp.js";
|
|
7
|
+
import { type McpServersConfigFile } from "./mcp-servers.js";
|
|
2
8
|
import { type RecoveryConfig } from "./recovery-config.js";
|
|
3
9
|
import { type CurationConfig } from "./curation-config.js";
|
|
4
10
|
import { type ManagementApiConfig, type ManagementApiConfigFile } from "./management-config.js";
|
|
@@ -62,6 +68,75 @@ export interface BrandConfig {
|
|
|
62
68
|
/** Accent color as a hex string driving the primary buttons + logo chip. */
|
|
63
69
|
accent: string;
|
|
64
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* What this instance shares with the host machine's Claude Code (#691).
|
|
73
|
+
*
|
|
74
|
+
* One key per concern, one vocabulary: `own` is paddock's, isolated inside the
|
|
75
|
+
* data dir; `host` is this machine's Claude Code. `own` everywhere is the
|
|
76
|
+
* guarantee an operator should be able to read straight off the config file —
|
|
77
|
+
* **nothing outside the data dir is read or written**.
|
|
78
|
+
*
|
|
79
|
+
* All five levers of #691's sequence now exist. Each is a key here rather than
|
|
80
|
+
* another meaning welded onto the Claude home, which is how the three incidents
|
|
81
|
+
* (#682, #683, #689) that motivated this all happened. What remains of #691 is
|
|
82
|
+
* step 6 — a place for a user to declare their OWN MCP servers, which is a
|
|
83
|
+
* different question from whose existing ones this instance borrows.
|
|
84
|
+
*/
|
|
85
|
+
export interface ClaudeConfig {
|
|
86
|
+
/**
|
|
87
|
+
* Whose session transcripts this instance uses. `own` (default) keeps them in
|
|
88
|
+
* each project's `.chats/`; `host` shares the user's real
|
|
89
|
+
* `~/.claude/projects/<encoded-cwd>/` folder live, in both directions.
|
|
90
|
+
*
|
|
91
|
+
* Driven by `PADDOCK_CLAUDE_TRANSCRIPTS`, then the `claude.transcripts:` key.
|
|
92
|
+
* See `transcripts.ts` for the mechanism and why it is not a repointed
|
|
93
|
+
* `CLAUDE_CONFIG_DIR`.
|
|
94
|
+
*/
|
|
95
|
+
transcripts: TranscriptsMode;
|
|
96
|
+
/**
|
|
97
|
+
* Whose Claude Code login this instance uses. `host` (the default, and the ONE
|
|
98
|
+
* key that does not default to `own`) also reads this machine's own login — the
|
|
99
|
+
* macOS Keychain entry on darwin, the user's `~/.claude/.credentials.json`
|
|
100
|
+
* elsewhere; `own` uses only what is inside paddock's own Claude home.
|
|
101
|
+
*
|
|
102
|
+
* Driven by `PADDOCK_CLAUDE_CREDENTIALS`, then the `claude.credentials:` key.
|
|
103
|
+
* See `claude-credentials.ts` for the mechanism, and for why sharing a login is
|
|
104
|
+
* the safe default when everything else here isolates.
|
|
105
|
+
*/
|
|
106
|
+
credentials: CredentialsMode;
|
|
107
|
+
/**
|
|
108
|
+
* Whose user-level instructions this instance loads — the user's `~/.claude`
|
|
109
|
+
* `CLAUDE.md`, `agents/`, `commands/` and `plugins/`. `own` (default) loads
|
|
110
|
+
* none of them; `host` symlinks them in, which is what every version before
|
|
111
|
+
* this one did unconditionally.
|
|
112
|
+
*
|
|
113
|
+
* Driven by `PADDOCK_CLAUDE_INSTRUCTIONS`, then the `claude.instructions:` key.
|
|
114
|
+
* See `claude-instructions.ts` — including the case AGAINST this default,
|
|
115
|
+
* which is real and is kept there rather than deleted.
|
|
116
|
+
*/
|
|
117
|
+
instructions: InstructionsMode;
|
|
118
|
+
/**
|
|
119
|
+
* Whether this instance runs the host machine's Claude Code hooks — shell
|
|
120
|
+
* commands `~/.claude/settings.json` binds to tool use and session lifecycle.
|
|
121
|
+
* `own` (default) writes paddock's own `settings.json` carrying every other key
|
|
122
|
+
* of the user's with `hooks` dropped; `host` symlinks theirs in whole.
|
|
123
|
+
*
|
|
124
|
+
* Driven by `PADDOCK_CLAUDE_HOOKS`, then the `claude.hooks:` key. The one lever
|
|
125
|
+
* here that governs code execution rather than data — see `claude-settings.ts`.
|
|
126
|
+
*/
|
|
127
|
+
hooks: HooksMode;
|
|
128
|
+
/**
|
|
129
|
+
* Whose MCP servers this instance's keepers get. `own` (default) attaches only
|
|
130
|
+
* the servers paddock itself provides; `host` also attaches the ones declared
|
|
131
|
+
* in the user's `~/.claude.json` — the top-level `mcpServers` plus any scoped
|
|
132
|
+
* to a project's own working directory.
|
|
133
|
+
*
|
|
134
|
+
* Driven by `PADDOCK_CLAUDE_MCP_SERVERS`, then the `claude.mcpServers:` key.
|
|
135
|
+
* The one lever whose source is NOT inside `~/.claude` — see `claude-mcp.ts`
|
|
136
|
+
* for why that made it break silently and separately from the other four.
|
|
137
|
+
*/
|
|
138
|
+
mcpServers: McpServersMode;
|
|
139
|
+
}
|
|
65
140
|
/** OpenAPI / Swagger-UI reference surface config (see PaddockConfig.openapi). */
|
|
66
141
|
export interface OpenApiConfig {
|
|
67
142
|
/** Mount the `/open-api` UI + `/open-api.json` spec. Opt-in — defaults false. */
|
|
@@ -96,8 +171,9 @@ export interface PaddockConfig {
|
|
|
96
171
|
/**
|
|
97
172
|
* Absolute path to the Claude home THIS INSTANCE owns — the directory whose
|
|
98
173
|
* `projects/<encoded-cwd>/` folders hold Claude Code's session transcripts.
|
|
99
|
-
*
|
|
100
|
-
*
|
|
174
|
+
* `<dataDir>/claude-home` unless an operator relocates it; paddock ALWAYS owns
|
|
175
|
+
* it (#691) and refuses to start if it resolves to the user's own `~/.claude`.
|
|
176
|
+
* See {@link resolveClaudeHome}.
|
|
101
177
|
*
|
|
102
178
|
* Resolved ONCE here (#588) rather than re-read from `process.env` at each call
|
|
103
179
|
* site, because it must be the SAME value everywhere: paddock's transcript
|
|
@@ -123,19 +199,15 @@ export interface PaddockConfig {
|
|
|
123
199
|
*/
|
|
124
200
|
legacyClaudeHome: string;
|
|
125
201
|
/**
|
|
126
|
-
*
|
|
127
|
-
*
|
|
202
|
+
* What this instance shares with the host's Claude Code (#691) — currently
|
|
203
|
+
* just `transcripts`. See {@link ClaudeConfig}.
|
|
128
204
|
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
* `claude` CLI session run in a directory paddock happens to also manage — and
|
|
134
|
-
* moving it is the destructive behaviour this flag exists to prevent.
|
|
135
|
-
*
|
|
136
|
-
* Derived, not configured: `resolve(claudeHome) !== resolve(legacyClaudeHome)`.
|
|
205
|
+
* Replaces the derived `ownsClaudeHome` flag, which was the single lever every
|
|
206
|
+
* distinct concern hung off. Paddock now always owns its home, so "may we
|
|
207
|
+
* write here?" has one answer and the question that actually varies — whose
|
|
208
|
+
* transcripts these are — is asked directly.
|
|
137
209
|
*/
|
|
138
|
-
|
|
210
|
+
claude: ClaudeConfig;
|
|
139
211
|
/** Provider-agnostic user-authentication config (see AUTH.md). */
|
|
140
212
|
auth: AuthConfig;
|
|
141
213
|
/** Voice-dictation (Whisper) capability (per-instance; default off). */
|
|
@@ -288,6 +360,31 @@ export interface PaddockConfig {
|
|
|
288
360
|
errors: string[];
|
|
289
361
|
warnings: string[];
|
|
290
362
|
};
|
|
363
|
+
/**
|
|
364
|
+
* MCP servers this instance declares for itself (#691 step 6), resolved from
|
|
365
|
+
* the top-level `mcpServers:` block with every `env:VAR_NAME` reference read
|
|
366
|
+
* from the environment. Every project's keeper is attached all of them, and
|
|
367
|
+
* they win a name collision against anything inherited via
|
|
368
|
+
* `claude.mcpServers: host`.
|
|
369
|
+
*
|
|
370
|
+
* **Holds resolved secret material** — an MCP server's `env` is where a stdio
|
|
371
|
+
* server's API token lives — exactly as {@link managementApi}'s client tokens
|
|
372
|
+
* do. It must never be serialised into an API response; `instance-config.ts`
|
|
373
|
+
* publishes only the fields in its own `FIELDS` table, and this is
|
|
374
|
+
* deliberately not one of them.
|
|
375
|
+
*/
|
|
376
|
+
mcpServers: McpServerDefs;
|
|
377
|
+
/**
|
|
378
|
+
* Operator-facing problems found while resolving {@link mcpServers}: a
|
|
379
|
+
* declaration paddock cannot carry (`errors`, server dropped) or one whose
|
|
380
|
+
* `env:` reference did not resolve / whose secret is inlined (`warnings`).
|
|
381
|
+
* Carried, not logged, for the same reason as
|
|
382
|
+
* {@link managementApiDiagnostics}. Never contains a value from the block.
|
|
383
|
+
*/
|
|
384
|
+
mcpServersDiagnostics: {
|
|
385
|
+
errors: string[];
|
|
386
|
+
warnings: string[];
|
|
387
|
+
};
|
|
291
388
|
/**
|
|
292
389
|
* Instance default for the hook-management MCP (Epic G / G5, GG-4) — the
|
|
293
390
|
* `mcp__paddock_manage__{list,set,remove}_hook` tools that let a project agent
|
|
@@ -385,8 +482,31 @@ export interface PaddockConfigFile {
|
|
|
385
482
|
stateDir?: string;
|
|
386
483
|
herdctlConfigPath?: string;
|
|
387
484
|
webDist?: string;
|
|
388
|
-
/** Claude home
|
|
485
|
+
/** Where paddock's OWN Claude home lives; beneath `CLAUDE_CONFIG_DIR` (#691). */
|
|
389
486
|
claudeHome?: string;
|
|
487
|
+
/**
|
|
488
|
+
* What this instance shares with the host's Claude Code (#691). Each key takes
|
|
489
|
+
* `own` or `host`; a matching `PADDOCK_CLAUDE_*` env var still overrides it
|
|
490
|
+
* (file < env). Absent ⇒ fully isolated.
|
|
491
|
+
*/
|
|
492
|
+
claude?: {
|
|
493
|
+
transcripts?: string;
|
|
494
|
+
credentials?: string;
|
|
495
|
+
instructions?: string;
|
|
496
|
+
hooks?: string;
|
|
497
|
+
mcpServers?: string;
|
|
498
|
+
};
|
|
499
|
+
/**
|
|
500
|
+
* MCP servers this instance declares for ITSELF (#691 step 6) — a sibling of
|
|
501
|
+
* `claude:`, not a member of it, because it answers a different question:
|
|
502
|
+
* `claude.mcpServers` borrows whatever this machine already has, while this
|
|
503
|
+
* says what paddock should have regardless.
|
|
504
|
+
*
|
|
505
|
+
* File-only, like `managementApi`: a map of servers does not express as a
|
|
506
|
+
* scalar env var. Token material belongs in the environment and is referenced
|
|
507
|
+
* as `env:VAR_NAME` from any string here — see `mcp-servers.ts`.
|
|
508
|
+
*/
|
|
509
|
+
mcpServers?: McpServersConfigFile;
|
|
390
510
|
auth?: {
|
|
391
511
|
mode?: string;
|
|
392
512
|
userHeader?: string;
|
|
@@ -561,35 +681,43 @@ export declare function loadPaddockConfig(): PaddockConfig;
|
|
|
561
681
|
*/
|
|
562
682
|
export declare function userClaudeHome(): string;
|
|
563
683
|
/**
|
|
564
|
-
* Resolve
|
|
684
|
+
* Resolve where paddock's OWN Claude home lives.
|
|
565
685
|
*
|
|
566
686
|
* Precedence, highest first:
|
|
567
687
|
*
|
|
568
|
-
* 1. **`
|
|
569
|
-
* `$HOME/.claude` restores the pre-#620 behaviour exactly, which is the
|
|
570
|
-
* documented escape hatch if the relocation causes trouble.
|
|
571
|
-
* 2. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
688
|
+
* 1. **`CLAUDE_CONFIG_DIR`** — the variable *Claude Code itself* resolves its
|
|
572
689
|
* home from. Honouring it is load-bearing, not a courtesy: herdctl
|
|
573
690
|
* deliberately refuses to clobber an operator-set `CLAUDE_CONFIG_DIR`
|
|
574
691
|
* (herdctl#423), so if paddock picked a different home the SDK would write
|
|
575
692
|
* transcripts to one tree while herdctl read from another — the exact
|
|
576
693
|
* split-brain #588 fixed. Deferring to it makes that state unreachable.
|
|
577
|
-
*
|
|
578
|
-
*
|
|
694
|
+
* 2. A `claudeHome:` key in the instance config file.
|
|
695
|
+
* 3. **`<dataDir>/claude-home`** — the default.
|
|
579
696
|
*
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
*
|
|
585
|
-
*
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
697
|
+
* ## Paddock always OWNS this home, and the one value it refuses
|
|
698
|
+
*
|
|
699
|
+
* `CLAUDE_HOME` used to sit above all of this, and setting it to `~/.claude` was
|
|
700
|
+
* the documented escape hatch back to the pre-#620 layout. It is gone (#691),
|
|
701
|
+
* because pointing the home at the user's own directory is what welded five
|
|
702
|
+
* unrelated concerns to one lever: it decided whose transcripts were at risk,
|
|
703
|
+
* which keychain entry was visible, where agent memory landed, and whether
|
|
704
|
+
* delete meant delete. Those are now separate keys (`claude:`), so the home no
|
|
705
|
+
* longer needs to move for any of them.
|
|
706
|
+
*
|
|
707
|
+
* Which leaves exactly one value that must not be allowed through, from any
|
|
708
|
+
* source: the user's own `~/.claude`. It re-welds the lever, and it re-breaks
|
|
709
|
+
* agent memory (#690) — the memory dir is `<claudeHome>/projects/<enc>/memory`,
|
|
710
|
+
* and the agent harness refuses to write to any path with a `.claude` component.
|
|
711
|
+
* So it REFUSES TO START rather than silently degrading, which is the same
|
|
712
|
+
* fail-closed shape as the bind-safety guard. `CLAUDE_CONFIG_DIR` cannot simply
|
|
713
|
+
* be ignored instead: it is Claude Code's own variable, and disagreeing with an
|
|
714
|
+
* operator-set value is the herdctl#423 split-brain again.
|
|
589
715
|
*
|
|
590
716
|
* NOT canonical()-ed: this exact string is what herdctl hands Claude Code as
|
|
591
717
|
* `CLAUDE_CONFIG_DIR`, and canonicalising would silently diverge from the
|
|
592
718
|
* literal path the two of them encode into `<home>/projects/<encoded-cwd>`.
|
|
719
|
+
* The refusal check DOES resolve both sides, so a symlinked or trailing-slash
|
|
720
|
+
* spelling of `~/.claude` cannot slip past it.
|
|
593
721
|
*
|
|
594
722
|
* This is the RESOLVER, not the accessor. It is read once by
|
|
595
723
|
* {@link loadPaddockConfig} into {@link PaddockConfig.claudeHome}; runtime code
|
|
@@ -597,5 +725,13 @@ export declare function userClaudeHome(): string;
|
|
|
597
725
|
* engine can never end up resolving two different homes (#588).
|
|
598
726
|
*/
|
|
599
727
|
export declare function resolveClaudeHome(dataDir: string, fromFile?: string): string;
|
|
728
|
+
/**
|
|
729
|
+
* Why `home` is unusable as paddock's own Claude home, or `undefined` if it is
|
|
730
|
+
* fine. Pure, so the whole decision is testable without touching `$HOME`.
|
|
731
|
+
*
|
|
732
|
+
* `fromEnv` only changes which fix is named first — the refusal itself is the
|
|
733
|
+
* same whichever layer supplied the value.
|
|
734
|
+
*/
|
|
735
|
+
export declare function claudeHomeRefusal(home: string, userHome: string, fromEnv: boolean): string | undefined;
|
|
600
736
|
/** Directory name of paddock's own Claude home inside the data dir (#620). */
|
|
601
737
|
export declare const DEFAULT_CLAUDE_HOME_DIRNAME = "claude-home";
|