@subshell-ai/plugin-claude-code 0.0.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,203 @@
1
+ Copyright 2026 Disaresta, LLC
2
+
3
+ Apache License
4
+ Version 2.0, January 2004
5
+ http://www.apache.org/licenses/
6
+
7
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
8
+
9
+ 1. Definitions.
10
+
11
+ "License" shall mean the terms and conditions for use, reproduction,
12
+ and distribution as defined by Sections 1 through 9 of this document.
13
+
14
+ "Licensor" shall mean the copyright owner or entity authorized by
15
+ the copyright owner that is granting the License.
16
+
17
+ "Legal Entity" shall mean the union of the acting entity and all
18
+ other entities that control, are controlled by, or are under common
19
+ control with that entity. For the purposes of this definition,
20
+ "control" means (i) the power, direct or indirect, to cause the
21
+ direction or management of such entity, whether by contract or
22
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
23
+ outstanding shares, or (iii) beneficial ownership of such entity.
24
+
25
+ "You" (or "Your") shall mean an individual or Legal Entity
26
+ exercising permissions granted by this License.
27
+
28
+ "Source" form shall mean the preferred form for making modifications,
29
+ including but not limited to software source code, documentation
30
+ source, and configuration files.
31
+
32
+ "Object" form shall mean any form resulting from mechanical
33
+ transformation or translation of a Source form, including but
34
+ not limited to compiled object code, generated documentation,
35
+ and conversions to other media types.
36
+
37
+ "Work" shall mean the work of authorship, whether in Source or
38
+ Object form, made available under the License, as indicated by a
39
+ copyright notice that is included in or attached to the work
40
+ (an example is provided in the Appendix below).
41
+
42
+ "Derivative Works" shall mean any work, whether in Source or Object
43
+ form, that is based on (or derived from) the Work and for which the
44
+ editorial revisions, annotations, elaborations, or other modifications
45
+ represent, as a whole, an original work of authorship. For the purposes
46
+ of this License, Derivative Works shall not include works that remain
47
+ separable from, or merely link (or bind by name) to the interfaces of,
48
+ the Work and Derivative Works thereof.
49
+
50
+ "Contribution" shall mean any work of authorship, including
51
+ the original version of the Work and any modifications or additions
52
+ to that Work or Derivative Works thereof, that is intentionally
53
+ submitted to Licensor for inclusion in the Work by the copyright owner
54
+ or by an individual or Legal Entity authorized to submit on behalf of
55
+ the copyright owner. For the purposes of this definition, "submitted"
56
+ means any form of electronic, verbal, or written communication sent
57
+ to the Licensor or its representatives, including but not limited to
58
+ communication on electronic mailing lists, source code control systems,
59
+ and issue tracking systems that are managed by, or on behalf of, the
60
+ Licensor for the purpose of discussing and improving the Work, but
61
+ excluding communication that is conspicuously marked or otherwise
62
+ designated in writing by the copyright owner as "Not a Contribution."
63
+
64
+ "Contributor" shall mean Licensor and any individual or Legal Entity
65
+ on behalf of whom a Contribution has been received by Licensor and
66
+ subsequently incorporated within the Work.
67
+
68
+ 2. Grant of Copyright License. Subject to the terms and conditions of
69
+ this License, each Contributor hereby grants to You a perpetual,
70
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
71
+ copyright license to reproduce, prepare Derivative Works of,
72
+ publicly display, publicly perform, sublicense, and distribute the
73
+ Work and such Derivative Works in Source or Object form.
74
+
75
+ 3. Grant of Patent License. Subject to the terms and conditions of
76
+ this License, each Contributor hereby grants to You a perpetual,
77
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
78
+ (except as stated in this section) patent license to make, have made,
79
+ use, offer to sell, sell, import, and otherwise transfer the Work,
80
+ where such license applies only to those patent claims licensable
81
+ by such Contributor that are necessarily infringed by their
82
+ Contribution(s) alone or by combination of their Contribution(s)
83
+ with the Work to which such Contribution(s) was submitted. If You
84
+ institute patent litigation against any entity (including a
85
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
86
+ or a Contribution incorporated within the Work constitutes direct
87
+ or contributory patent infringement, then any patent licenses
88
+ granted to You under this License for that Work shall terminate
89
+ as of the date such litigation is filed.
90
+
91
+ 4. Redistribution. You may reproduce and distribute copies of the
92
+ Work or Derivative Works thereof in any medium, with or without
93
+ modifications, and in Source or Object form, provided that You
94
+ meet the following conditions:
95
+
96
+ (a) You must give any other recipients of the Work or
97
+ Derivative Works a copy of this License; and
98
+
99
+ (b) You must cause any modified files to carry prominent notices
100
+ stating that You changed the files; and
101
+
102
+ (c) You must retain, in the Source form of any Derivative Works
103
+ that You distribute, all copyright, patent, trademark, and
104
+ attribution notices from the Source form of the Work,
105
+ excluding those notices that do not pertain to any part of
106
+ the Derivative Works; and
107
+
108
+ (d) If the Work includes a "NOTICE" text file as part of its
109
+ distribution, then any Derivative Works that You distribute must
110
+ include a readable copy of the attribution notices contained
111
+ within such NOTICE file, excluding those notices that do not
112
+ pertain to any part of the Derivative Works, in at least one
113
+ of the following places: within a NOTICE text file distributed
114
+ as part of the Derivative Works; within the Source form or
115
+ documentation, if provided along with the Derivative Works; or,
116
+ within a display generated by the Derivative Works, if and
117
+ wherever such third-party notices normally appear. The contents
118
+ of the NOTICE file are for informational purposes only and
119
+ do not modify the License. You may add Your own attribution
120
+ notices within Derivative Works that You distribute, alongside
121
+ or as an addendum to the NOTICE text from the Work, provided
122
+ that such additional attribution notices cannot be construed
123
+ as modifying the License.
124
+
125
+ You may add Your own copyright statement to Your modifications and
126
+ may provide additional or different license terms and conditions
127
+ for use, reproduction, or distribution of Your modifications, or
128
+ for any such Derivative Works as a whole, provided Your use,
129
+ reproduction, and distribution of the Work otherwise complies with
130
+ the conditions stated in this License.
131
+
132
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
133
+ any Contribution intentionally submitted for inclusion in the Work
134
+ by You to the Licensor shall be under the terms and conditions of
135
+ this License, without any additional terms or conditions.
136
+ Notwithstanding the above, nothing herein shall supersede or modify
137
+ the terms of any separate license agreement you may have executed
138
+ with Licensor regarding such Contributions.
139
+
140
+ 6. Trademarks. This License does not grant permission to use the trade
141
+ names, trademarks, service marks, or product names of the Licensor,
142
+ except as required for reasonable and customary use in describing the
143
+ origin of the Work and reproducing the content of the NOTICE file.
144
+
145
+ 7. Disclaimer of Warranty. Unless required by applicable law or
146
+ agreed to in writing, Licensor provides the Work (and each
147
+ Contributor provides its Contributions) on an "AS IS" BASIS,
148
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
149
+ implied, including, without limitation, any warranties or conditions
150
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
151
+ PARTICULAR PURPOSE. You are solely responsible for determining the
152
+ appropriateness of using or redistributing the Work and assume any
153
+ risks associated with Your exercise of permissions under this License.
154
+
155
+ 8. Limitation of Liability. In no event and under no legal theory,
156
+ whether in tort (including negligence), contract, or otherwise,
157
+ unless required by applicable law (such as deliberate and grossly
158
+ negligent acts) or agreed to in writing, shall any Contributor be
159
+ liable to You for damages, including any direct, indirect, special,
160
+ incidental, or consequential damages of any character arising as a
161
+ result of this License or out of the use or inability to use the
162
+ Work (including but not limited to damages for loss of goodwill,
163
+ work stoppage, computer failure or malfunction, or any and all
164
+ other commercial damages or losses), even if such Contributor
165
+ has been advised of the possibility of such damages.
166
+
167
+ 9. Accepting Warranty or Additional Liability. While redistributing
168
+ the Work or Derivative Works thereof, You may choose to offer,
169
+ and charge a fee for, acceptance of support, warranty, indemnity,
170
+ or other liability obligations and/or rights consistent with this
171
+ License. However, in accepting such obligations, You may act only
172
+ on Your own behalf and on Your sole responsibility, not on behalf
173
+ of any other Contributor, and only if You agree to indemnify,
174
+ defend, and hold each Contributor harmless for any liability
175
+ incurred by, or claims asserted against, such Contributor by reason
176
+ of your accepting any such warranty or additional liability.
177
+
178
+ END OF TERMS AND CONDITIONS
179
+
180
+ APPENDIX: How to apply the Apache License to your work.
181
+
182
+ To apply the Apache License to your work, attach the following
183
+ boilerplate notice, with the fields enclosed by brackets "[]"
184
+ replaced with your own identifying information. (Don't include
185
+ the brackets!) The text should be enclosed in the appropriate
186
+ comment syntax for the file format. We also recommend that a
187
+ file or class name and description of purpose be included on the
188
+ same "printed page" as the copyright notice for easier
189
+ identification within third-party archives.
190
+
191
+ Copyright [yyyy] [name of copyright owner]
192
+
193
+ Licensed under the Apache License, Version 2.0 (the "License");
194
+ you may not use this file except in compliance with the License.
195
+ You may obtain a copy of the License at
196
+
197
+ http://www.apache.org/licenses/LICENSE-2.0
198
+
199
+ Unless required by applicable law or agreed to in writing, software
200
+ distributed under the License is distributed on an "AS IS" BASIS,
201
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
202
+ See the License for the specific language governing permissions and
203
+ limitations under the License.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../../plugin-api/dist/types-90lJTTx6.d.ts
1
+ //#region ../../plugin-api/dist/types-CSNepTNr.d.ts
2
2
  //#region src/types.d.ts
3
3
  /**
4
4
  * The contract a Subshell plugin implements.
@@ -81,13 +81,46 @@ interface BuildCommandInput {
81
81
  * declares the `resume` capability. `mode: "start"` means the conversation
82
82
  * is NEW and must be created under exactly this id (pin it, because the host
83
83
  * stores it and later resumes by it); `mode: "resume"` names an EXISTING
84
- * conversation to continue (the host only asks after {@link HarnessResume.canResume}
85
- * confirmed it survives).
84
+ * conversation to continue (the host only asks after {@link HarnessResume.resumePath}
85
+ * pointed at a file the host found there).
86
86
  */
87
87
  harnessSession?: {
88
88
  id: string;
89
89
  mode: "start" | "resume";
90
90
  };
91
+ /**
92
+ * How a harness hook re-enters the subshell binary on the PANE's machine, as
93
+ * the host resolved it for that machine: `{ command, args }` with the
94
+ * reporting subcommand already appended, so a plugin adds only its own verb
95
+ * words. Splice it into whatever hook mechanism the harness has.
96
+ *
97
+ * A hook runs where the pane runs, and the only program guaranteed to exist
98
+ * there is the binary that launched the pane — which is why this arrives
99
+ * from the host rather than being a command a plugin writes for itself. The
100
+ * hooks here were once `bun -e '<inlined JS>'`, and every machine without a
101
+ * bun on its PATH (i.e. most of them) opened its harness on
102
+ * `bun: command not found`.
103
+ *
104
+ * Undefined means the host could not resolve one. OMIT the hooks then: a
105
+ * command the pane cannot run costs the same signal and adds an error to
106
+ * every session.
107
+ */
108
+ reporter?: ReporterSpec;
109
+ }
110
+ /**
111
+ * A command prefix that re-enters the subshell binary on a pane's machine,
112
+ * ready for a plugin's own verb words to be appended.
113
+ *
114
+ * Shaped like {@link McpLaunchSpec} and resolved from the same host question
115
+ * ("am I a compiled binary, or is `bun` running my entry script?"), but kept
116
+ * separate because they carry different subcommands and the MCP one is
117
+ * operator-overridable while this one is not.
118
+ */
119
+ interface ReporterSpec {
120
+ /** Executable to run (the binary itself, or the interpreter running it). */
121
+ command: string;
122
+ /** Everything before the plugin's verb words (any entry script, then the subcommand). */
123
+ args: string[];
91
124
  }
92
125
  /**
93
126
  * Restart-resume, implemented only by plugins that can continue a previous
@@ -95,18 +128,31 @@ interface BuildCommandInput {
95
128
  *
96
129
  * The ids here are HARNESS conversation ids (a claude transcript uuid, say),
97
130
  * never subshell ids: the host pins {@link allocateHarnessSessionId} at launch,
98
- * stores it on the subshell row, and consults {@link canResume} before every
131
+ * stores it on the subshell row, and consults {@link resumePath} before every
99
132
  * restart to choose between continuing and starting fresh.
133
+ *
134
+ * Both members are PURE, and that is the load-bearing property (spec
135
+ * 2026-09-10 §5): a resume runs on the CONTROL PLANE, for a pane that lives on
136
+ * a machine the plane cannot see. Computing (rather than checking) is what
137
+ * lets one plugin implementation serve local and remote restarts identically.
100
138
  */
101
139
  interface HarnessResume {
102
140
  /** Allocates the HARNESS conversation id to pin at launch (a uuid for claude). */
103
141
  allocateHarnessSessionId(): string;
104
142
  /**
105
- * Whether the harness conversation `harnessSessionId` (last run in `cwd`)
106
- * still exists and can be resumed. False means the host launches a fresh
107
- * conversation rather than handing the harness an id it would reject.
143
+ * Where the resumable transcript would be, given the target machine's
144
+ * environment. PURE: it computes a path and never touches a filesystem, so
145
+ * the control plane can build it for a machine it cannot see. The HOST
146
+ * checks existence.
108
147
  */
109
- canResume(harnessSessionId: string, cwd: string): boolean;
148
+ resumePath(harnessSessionId: string, cwd: string, hostEnv: HostEnv): string;
149
+ }
150
+ /** The parts of a target machine's environment a plugin may compute against. */
151
+ interface HostEnv {
152
+ /** The node's home directory */
153
+ homeDir: string;
154
+ /** Values for the variables this plugin's manifest declared it needs */
155
+ env: Record<string, string>;
110
156
  }
111
157
  /** How to spawn the `subshell mcp` stdio server. */
112
158
  interface McpLaunchSpec {
@@ -299,33 +345,22 @@ interface SubshellManifest {
299
345
  detect?: DetectSpec;
300
346
  /** Install guidance shown when the binary is missing */
301
347
  install?: InstallSpec;
348
+ /**
349
+ * Environment variables this plugin computes against (spec 2026-09-10 §5).
350
+ *
351
+ * The node reports the VALUES of exactly these names at `ready`, so the
352
+ * reported set grows by declaration rather than a machine shipping its
353
+ * whole environment to the control plane. Declaring data is deliberate: a
354
+ * WRONG name is silent, yielding an absent key and the plugin's own
355
+ * fallback, i.e. a resume that quietly never offers itself (§11).
356
+ */
357
+ hostEnv?: string[];
302
358
  }
303
359
  //#endregion
304
360
  //#region src/manifest.d.ts
305
361
  declare const manifest: SubshellManifest;
306
362
  //#endregion
307
363
  //#region src/index.d.ts
308
- /** The `--settings` hooks object injected into every Claude Code launch. */
309
- declare const ATTENTION_HOOKS: {
310
- readonly Stop: readonly [{
311
- readonly hooks: readonly [{
312
- readonly type: "command";
313
- readonly command: string;
314
- }];
315
- }];
316
- readonly Notification: readonly [{
317
- readonly hooks: readonly [{
318
- readonly type: "command";
319
- readonly command: string;
320
- }];
321
- }];
322
- readonly SessionStart: readonly [{
323
- readonly hooks: readonly [{
324
- readonly type: "command";
325
- readonly command: string;
326
- }];
327
- }];
328
- };
329
364
  /**
330
365
  * Built-in harness: Claude Code.
331
366
  *
@@ -345,4 +380,4 @@ declare const ATTENTION_HOOKS: {
345
380
  */
346
381
  declare const createPlugin: PluginFactory;
347
382
  //#endregion
348
- export { ATTENTION_HOOKS, createPlugin as default, manifest };
383
+ export { createPlugin as default, manifest };
package/dist/index.js CHANGED
@@ -1,7 +1,5 @@
1
- import { existsSync } from "node:fs";
2
- import { homedir } from "node:os";
3
1
  import { join, resolve } from "node:path";
4
- //#region ../../plugin-api/dist/manifest-im30YVIq.js
2
+ //#region ../../plugin-api/dist/manifest-UFScnYqn.js
5
3
  /** Every {@link PluginType}, for anything that has to iterate or validate them. */
6
4
  const PLUGIN_TYPES = ["agent-harness", "terminal"];
7
5
  /**
@@ -62,6 +60,11 @@ function parseManifest(pkgJson) {
62
60
  docsUrl: i.docsUrl
63
61
  };
64
62
  }
63
+ let hostEnv;
64
+ if (block.hostEnv !== void 0) {
65
+ if (!Array.isArray(block.hostEnv) || !block.hostEnv.every((k) => typeof k === "string" && k.trim() !== "")) return { error: "`subshell.hostEnv` must be an array of environment variable names" };
66
+ hostEnv = [...block.hostEnv];
67
+ }
65
68
  return {
66
69
  apiVersion: block.apiVersion,
67
70
  id: block.id,
@@ -71,7 +74,8 @@ function parseManifest(pkgJson) {
71
74
  ...typeof block.icon === "string" ? { icon: block.icon } : {},
72
75
  entry: block.entry,
73
76
  ...detect ? { detect } : {},
74
- ...install ? { install } : {}
77
+ ...install ? { install } : {},
78
+ ...hostEnv ? { hostEnv } : {}
75
79
  };
76
80
  }
77
81
  //#endregion
@@ -115,7 +119,7 @@ function validateGenericProfile(profile) {
115
119
  const parsed = parseManifest({
116
120
  name: "@subshell-ai/plugin-claude-code",
117
121
  description: "Claude Code harness plugin for Subshell",
118
- version: "0.0.1",
122
+ version: "1.0.1",
119
123
  license: "Apache-2.0",
120
124
  type: "module",
121
125
  main: "dist/index.js",
@@ -124,6 +128,13 @@ const parsed = parseManifest({
124
128
  "import": "./dist/index.js"
125
129
  } },
126
130
  types: "dist/index.d.ts",
131
+ homepage: "https://subshell.sh",
132
+ repository: {
133
+ "type": "git",
134
+ "url": "git+https://github.com/subshell-ai/subshell.git",
135
+ "directory": "packages/plugins/claude-code"
136
+ },
137
+ bugs: { "url": "https://github.com/subshell-ai/subshell/issues" },
127
138
  scripts: {
128
139
  "build": "tsdown",
129
140
  "build:dev": "hash-runner",
@@ -162,6 +173,7 @@ const parsed = parseManifest({
162
173
  ".npm-global/bin/claude"
163
174
  ]
164
175
  },
176
+ "hostEnv": ["CLAUDE_CONFIG_DIR"],
165
177
  "install": {
166
178
  "command": "curl -fsSL https://claude.ai/install.sh | bash",
167
179
  "docsUrl": "https://code.claude.com/docs/en/setup"
@@ -340,58 +352,56 @@ const SUGGESTED_FLAGS = [
340
352
  }
341
353
  ];
342
354
  /**
343
- * Fire-and-forget attention reporting. Each hook runs `bun -e` (bun is on the
344
- * pane PATH — the image ships it, and no curl exists there) and POSTs the
345
- * subshell's own bearer to the attention endpoint; the server gates delivery
346
- * on the subshell's bell and derives the "waiting for you" state. The env
347
- * vars are baked into the pane by the backend (`subshellMcpEnv`), and every
348
- * failure path is swallowed: a missing hook event costs one notification,
349
- * never a broken subshell turn. SUBSHELL_BASE_URL must be reachable from inside
350
- * the pane's network — in the container that means the published port, which
351
- * compose already passes via APP_BASE_URL.
355
+ * One hook command line: the host-resolved reporter, then this hook's verb
356
+ * words, every element POSIX-quoted.
357
+ *
358
+ * A hook runs on the PANE's machine, so the command has to name something that
359
+ * exists there. It used to be `bun -e '<inlined JS>'` — bun was on the pane
360
+ * PATH because the container image shipped it, and no curl was there to use
361
+ * instead. That assumption did not survive leaving the container: on a desktop
362
+ * install the machine has the subshell binary and nothing else, and Claude Code
363
+ * opened every session with `/bin/sh: bun: command not found` while
364
+ * notifications and conversation identity silently never worked.
365
+ *
366
+ * So the reporting moved INTO the binary (`<self> report …`), and what the
367
+ * plugin composes is an argv rather than a program. Bounds, credentials and
368
+ * which fields travel are that subcommand's contract now, which is also why
369
+ * this function no longer needs to know the pane env exists.
352
370
  */
353
- const attentionPing = (kind) => `bun -e 'fetch(process.env.SUBSHELL_BASE_URL+"/api/subshells/"+process.env.SUBSHELL_ID+"/attention",{method:"POST",headers:{authorization:"Bearer "+process.env.SUBSHELL_API_KEY,"content-type":"application/json"},body:JSON.stringify({kind:${JSON.stringify(kind)}}),signal:AbortSignal.timeout(5000)}).catch(()=>{}).finally(()=>process.exit(0))'`;
371
+ const reporterHook = (host, reporter, ...verb) => [
372
+ reporter.command,
373
+ ...reporter.args,
374
+ ...verb
375
+ ].map((word) => host.shellQuote(word)).join(" ");
354
376
  /**
355
- * Conversation-identity reporting. The restart-resume pin (`--session-id`
356
- * at launch) only survives while the pane keeps that ONE conversation — but
357
- * /clear, /resume <other>, and /fork start a DIFFERENT transcript id in-pane
358
- * and nothing else tells the server, so the next restart would resurrect a
359
- * stale conversation (observed 2026-09-03). The SessionStart hook fires on
360
- * every such transition (source: startup|resume|clear|compact|fork).
361
- *
362
- * Unlike the attention pings, THIS command reads stdin — the deliberate,
363
- * documented exception to the no-stdin rule (whose rationale is that
364
- * Stop/Notification payloads carry conversation text): the SessionStart
365
- * payload is pure metadata ({session_id, transcript_path, cwd, source,…})
366
- * and ONLY `session_id` is forwarded; the response is fire-and-forget like
367
- * the pings — every failure path is swallowed.
377
+ * The `--settings` hooks object for one launch.
368
378
  *
369
- * Both awaits are bounded: a self-kill timer caps the WHOLE hook (4 s — an
370
- * unclosed stdin must never stall the pane start, and a SessionStart hook
371
- * blocks the harness until it exits) and a 2 s fetch timeout caps the POST
372
- * (a lost report is harmless — the next transition re-reports).
379
+ * - `Stop` / `Notification` — fire-and-forget attention reporting. The server
380
+ * gates delivery on the subshell's bell and derives the "waiting for you"
381
+ * state; a missed event costs one notification, never a broken turn.
382
+ * - `SessionStart` — conversation identity. The restart-resume pin
383
+ * (`--session-id` at launch) only survives while the pane keeps that ONE
384
+ * conversation, but /clear, /resume <other> and /fork start a DIFFERENT
385
+ * transcript id in-pane and nothing else tells the server, so the next
386
+ * restart would resurrect a stale conversation (observed 2026-09-03). This
387
+ * fires on every such transition (source: startup|resume|clear|compact|fork)
388
+ * and is the one hook that reads stdin — the reporter forwards `session_id`
389
+ * from that payload and nothing else.
373
390
  */
374
- const sessionReportPing = () => "bun -e 'setTimeout(()=>process.exit(0),4000);try{const j=JSON.parse(await Bun.stdin.text());if(j.session_id)await fetch(process.env.SUBSHELL_BASE_URL+\"/api/subshells/\"+process.env.SUBSHELL_ID+\"/harness-session\",{method:\"POST\",headers:{authorization:\"Bearer \"+process.env.SUBSHELL_API_KEY,\"content-type\":\"application/json\"},body:JSON.stringify({sessionId:j.session_id}),signal:AbortSignal.timeout(2000)})}catch{}process.exit(0)'";
375
- /** The `--settings` hooks object injected into every Claude Code launch. */
376
- const ATTENTION_HOOKS = {
391
+ const attentionHooks = (host, reporter) => ({
377
392
  Stop: [{ hooks: [{
378
393
  type: "command",
379
- command: attentionPing("turn_complete")
394
+ command: reporterHook(host, reporter, "attention", "turn_complete")
380
395
  }] }],
381
396
  Notification: [{ hooks: [{
382
397
  type: "command",
383
- command: attentionPing("needs_attention")
398
+ command: reporterHook(host, reporter, "attention", "needs_attention")
384
399
  }] }],
385
400
  SessionStart: [{ hooks: [{
386
401
  type: "command",
387
- command: sessionReportPing()
402
+ command: reporterHook(host, reporter, "session")
388
403
  }] }]
389
- };
390
- /** Claude's state dir: the documented override, else `~/.claude`. */
391
- function claudeConfigDir() {
392
- const override = process.env.CLAUDE_CONFIG_DIR?.trim();
393
- return override ? resolve(override) : join(homedir(), ".claude");
394
- }
404
+ });
395
405
  /** Claude's transcript folder name for a project dir: every non-alphanumeric → `-`. */
396
406
  function projectSlug(cwd) {
397
407
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -413,7 +423,7 @@ function projectSlug(cwd) {
413
423
  * version probe, shell quoting, a namespaced logger). See
414
424
  * `@subshell-ai/plugin-api`.
415
425
  */
416
- const createPlugin = (_host) => ({
426
+ const createPlugin = (host) => ({
417
427
  capabilities: () => [
418
428
  "mcp",
419
429
  "resume",
@@ -431,18 +441,29 @@ const createPlugin = (_host) => ({
431
441
  */
432
442
  resume: {
433
443
  allocateHarnessSessionId: () => crypto.randomUUID(),
434
- canResume: (harnessSessionId, cwd) => existsSync(join(claudeConfigDir(), "projects", projectSlug(cwd), `${harnessSessionId}.jsonl`))
444
+ /**
445
+ * Claude's state dir: the documented override, else `<homeDir>/.claude`.
446
+ * Both halves come from the TARGET machine's `ready` report, never from
447
+ * this process, and no filesystem is touched (spec 2026-09-10 §5) — that
448
+ * is what lets the control plane build the path for a node it cannot
449
+ * see. The host stats the result and decides; this member cannot answer
450
+ * whether the transcript exists.
451
+ */
452
+ resumePath: (harnessSessionId, cwd, hostEnv) => {
453
+ const override = hostEnv.env.CLAUDE_CONFIG_DIR?.trim();
454
+ const configDir = override ? resolve(override) : join(hostEnv.homeDir, ".claude");
455
+ return join(configDir, "projects", projectSlug(cwd), `${harnessSessionId}.jsonl`);
456
+ }
435
457
  },
436
458
  buildCommand(input) {
437
- const { binary, profile, subshellName, extraFlags, mcp, harnessSession } = input;
459
+ const { binary, profile, subshellName, extraFlags, mcp, harnessSession, reporter } = input;
438
460
  const args = [binary];
439
461
  if (mcp?.args) args.push(...mcp.args);
440
462
  if (harnessSession) args.push(harnessSession.mode === "resume" ? "--resume" : "--session-id", harnessSession.id);
441
- const settings = {
442
- ...profile.settings ?? {},
443
- hooks: ATTENTION_HOOKS
444
- };
445
- args.push("--settings", JSON.stringify(settings));
463
+ const settings = { ...profile.settings ?? {} };
464
+ if (reporter) settings.hooks = attentionHooks(host, reporter);
465
+ else host.log.warn("no reporter resolved for this launch: attention and conversation-identity hooks are omitted");
466
+ if (Object.keys(settings).length > 0) args.push("--settings", JSON.stringify(settings));
446
467
  if (subshellName) args.push("--name", subshellName);
447
468
  for (const flag of profile.flags) args.push(flag);
448
469
  if (extraFlags) args.push(...extraFlags);
@@ -487,4 +508,4 @@ const createPlugin = (_host) => ({
487
508
  }
488
509
  });
489
510
  //#endregion
490
- export { ATTENTION_HOOKS, createPlugin as default, manifest };
511
+ export { createPlugin as default, manifest };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@subshell-ai/plugin-claude-code",
3
3
  "description": "Claude Code harness plugin for Subshell",
4
- "version": "0.0.1",
4
+ "version": "1.0.1",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -12,6 +12,15 @@
12
12
  }
13
13
  },
14
14
  "types": "dist/index.d.ts",
15
+ "homepage": "https://subshell.sh",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/subshell-ai/subshell.git",
19
+ "directory": "packages/plugins/claude-code"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/subshell-ai/subshell/issues"
23
+ },
15
24
  "scripts": {
16
25
  "build": "tsdown",
17
26
  "build:dev": "hash-runner",
@@ -42,7 +51,7 @@
42
51
  "type": "agent-harness",
43
52
  "name": "Claude Code",
44
53
  "description": "Anthropic's agentic coding assistant (interactive CLI)",
45
- "icon": "\ud83e\udd16",
54
+ "icon": "🤖",
46
55
  "entry": "dist/index.js",
47
56
  "detect": {
48
57
  "binaryName": "claude",
@@ -54,6 +63,9 @@
54
63
  ".npm-global/bin/claude"
55
64
  ]
56
65
  },
66
+ "hostEnv": [
67
+ "CLAUDE_CONFIG_DIR"
68
+ ],
57
69
  "install": {
58
70
  "command": "curl -fsSL https://claude.ai/install.sh | bash",
59
71
  "docsUrl": "https://code.claude.com/docs/en/setup"