@subshell-ai/plugin-claude-code 1.0.0 → 2.0.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/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-CzNsDcbd.d.ts
1
+ //#region ../../plugin-api/dist/types-B_wiApwK.d.ts
2
2
  //#region src/types.d.ts
3
3
  /**
4
4
  * The contract a Subshell plugin implements.
@@ -27,9 +27,9 @@ type PluginType = "agent-harness" | "terminal";
27
27
  * branches on {@link PluginType}.
28
28
  */
29
29
  type PluginCapability = "mcp" | "resume" | "attention" | "settings";
30
- /** A harness profile as defined by the user (decoded JSON blobs). */
31
- interface ProfileDefinition {
32
- /** Human-friendly profile name */
30
+ /** A harness preset as defined by the user (decoded JSON blobs). */
31
+ interface PresetDefinition {
32
+ /** Human-friendly preset name */
33
33
  name: string;
34
34
  /** Optional longer description */
35
35
  description?: string | null;
@@ -39,21 +39,21 @@ interface ProfileDefinition {
39
39
  flags: string[];
40
40
  /** Settings blob passed to the harness (e.g. claude --settings JSON) */
41
41
  settings: Record<string, unknown> | null;
42
- /** If true, only this profile's config sources apply (isolation) */
42
+ /** If true, only this preset's config sources apply (isolation) */
43
43
  configIsolation: boolean;
44
- /** If true, new subshells from this profile auto-restart on exit */
44
+ /** If true, new subshells from this preset auto-restart on exit */
45
45
  restartOnExit?: boolean;
46
46
  }
47
- /** Snapshot of a single field-level validation error on a profile. */
48
- interface ProfileValidationIssue {
47
+ /** Snapshot of a single field-level validation error on a preset. */
48
+ interface PresetValidationIssue {
49
49
  /** Field name (e.g. "name", "env", "settings") */
50
50
  field: string;
51
51
  /** Human-readable problem description */
52
52
  message: string;
53
53
  }
54
- interface ProfileValidationResult {
54
+ interface PresetValidationResult {
55
55
  valid: boolean;
56
- issues: ProfileValidationIssue[];
56
+ issues: PresetValidationIssue[];
57
57
  }
58
58
  /** Everything a plugin needs to build a launch command. */
59
59
  interface BuildCommandInput {
@@ -61,8 +61,8 @@ interface BuildCommandInput {
61
61
  binary: string;
62
62
  /** Working directory the harness runs in */
63
63
  cwd: string;
64
- /** The validated profile being used */
65
- profile: ProfileDefinition;
64
+ /** The validated preset being used */
65
+ preset: PresetDefinition;
66
66
  /** Subshell display name ("" = let the harness pick a default) */
67
67
  subshellName: string;
68
68
  /** Any additional CLI flags from route/request context */
@@ -88,6 +88,39 @@ interface BuildCommandInput {
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
@@ -136,12 +169,12 @@ interface McpRegistration {
136
169
  args?: string[];
137
170
  /**
138
171
  * Extra pane env the harness needs to discover the file (e.g. OPENCODE_CONFIG).
139
- * Consumed by the HOST, which bakes it into the pane env ahead of profile env;
172
+ * Consumed by the HOST, which bakes it into the pane env ahead of preset env;
140
173
  * plugins never read it back.
141
174
  */
142
175
  env?: Record<string, string>;
143
176
  }
144
- /** One copy-paste line shown in the profile editor for manual-setup harnesses. */
177
+ /** One copy-paste line shown in the preset editor for manual-setup harnesses. */
145
178
  interface McpSetupStep {
146
179
  /** What the user should do, and where the text goes */
147
180
  label: string;
@@ -226,8 +259,8 @@ interface PluginHost {
226
259
  interface SubshellPlugin {
227
260
  /** Builds the argv (no shell) used to launch a subshell. */
228
261
  buildCommand(input: BuildCommandInput): string[];
229
- /** Validates a profile definition before saving. */
230
- validateProfile(profile: ProfileDefinition): ProfileValidationResult;
262
+ /** Validates a preset definition before saving. */
263
+ validatePreset(preset: PresetDefinition): PresetValidationResult;
231
264
  /** Which optional members below are meaningful on this plugin. */
232
265
  capabilities(): PluginCapability[];
233
266
  /** Maps a harness exit code to a human label (null = unknown). */
@@ -259,14 +292,14 @@ interface SubshellPlugin {
259
292
  mcpRegistration?(launch: McpLaunchSpec, configPath: string): McpRegistration;
260
293
  /** How users obtain the subshell MCP tools here. Declare the `mcp` capability with it. */
261
294
  mcpSetup?(launch: McpLaunchSpec): McpSetupInfo;
262
- /** Settings rendered in the PROFILE editor and stored on the profile. */
263
- profileSettings?(): SettingsField[];
264
- /** Known extra env var suggestions for the profile editor. */
295
+ /** Settings rendered in the PRESET editor and stored on the preset. */
296
+ presetSettings?(): SettingsField[];
297
+ /** Known extra env var suggestions for the preset editor. */
265
298
  suggestedEnv?(): {
266
299
  key: string;
267
300
  description: string;
268
301
  }[];
269
- /** Known CLI flag suggestions for the profile editor. */
302
+ /** Known CLI flag suggestions for the preset editor. */
270
303
  suggestedFlags?(): {
271
304
  flag: string;
272
305
  description: string;
@@ -304,7 +337,11 @@ interface SubshellManifest {
304
337
  name: string;
305
338
  /** One-line description shown in the UI */
306
339
  description: string;
307
- /** Optional emoji/glyph */
340
+ /**
341
+ * Relative path to this plugin's icon file, e.g. `icon.svg`. Served by
342
+ * the control plane at `/api/plugins/<id>/icon`; absent means the UI
343
+ * renders a monogram instead.
344
+ */
308
345
  icon?: string;
309
346
  /** Module to import, relative to the package directory */
310
347
  entry: string;
@@ -328,34 +365,13 @@ interface SubshellManifest {
328
365
  declare const manifest: SubshellManifest;
329
366
  //#endregion
330
367
  //#region src/index.d.ts
331
- /** The `--settings` hooks object injected into every Claude Code launch. */
332
- declare const ATTENTION_HOOKS: {
333
- readonly Stop: readonly [{
334
- readonly hooks: readonly [{
335
- readonly type: "command";
336
- readonly command: string;
337
- }];
338
- }];
339
- readonly Notification: readonly [{
340
- readonly hooks: readonly [{
341
- readonly type: "command";
342
- readonly command: string;
343
- }];
344
- }];
345
- readonly SessionStart: readonly [{
346
- readonly hooks: readonly [{
347
- readonly type: "command";
348
- readonly command: string;
349
- }];
350
- }];
351
- };
352
368
  /**
353
369
  * Built-in harness: Claude Code.
354
370
  *
355
371
  * Launch shape:
356
- * claude --settings <json> --name <subshell> [profile flags] [extra flags]
372
+ * claude --settings <json> --name <subshell> [preset flags] [extra flags]
357
373
  * Settings are passed as a JSON string via `--settings` (per-invocation, so
358
- * profile config never needs to write into the user's real ~/.claude).
374
+ * preset config never needs to write into the user's real ~/.claude).
359
375
  *
360
376
  * Identity, detection and install guidance live in this package's
361
377
  * package.json `subshell` block, NOT here: the host reads them without
@@ -368,4 +384,4 @@ declare const ATTENTION_HOOKS: {
368
384
  */
369
385
  declare const createPlugin: PluginFactory;
370
386
  //#endregion
371
- export { ATTENTION_HOOKS, createPlugin as default, manifest };
387
+ export { createPlugin as default, manifest };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { join, resolve } from "node:path";
2
- //#region ../../plugin-api/dist/manifest-UFScnYqn.js
2
+ //#region ../../plugin-api/dist/manifest-Czd9ea1g.js
3
3
  /** Every {@link PluginType}, for anything that has to iterate or validate them. */
4
4
  const PLUGIN_TYPES = ["agent-harness", "terminal"];
5
5
  /**
@@ -10,6 +10,17 @@ const PLUGIN_TYPES = ["agent-harness", "terminal"];
10
10
  */
11
11
  const MCP_SERVER_NAME = "subshell";
12
12
  /**
13
+ * Image formats a plugin icon may be. The server maps these to a Content-Type
14
+ * from a fixed table rather than sniffing the file, so this list and that
15
+ * table are one decision: adding a format here without adding it there leaves
16
+ * an icon that parses and then 500s when something asks for it.
17
+ */
18
+ const ICON_EXTENSIONS = [
19
+ ".svg",
20
+ ".png",
21
+ ".webp"
22
+ ];
23
+ /**
13
24
  * Ids become directory names under `<dataDir>/plugins/`, so they are path
14
25
  * segments and nothing else: no separators, no traversal, no surprises from a
15
26
  * case-insensitive filesystem.
@@ -32,13 +43,18 @@ function parseManifest(pkgJson) {
32
43
  const block = pkgJson.subshell;
33
44
  if (!isRecord(block)) return { error: "package.json has no `subshell` block, so it is not a Subshell plugin" };
34
45
  if (typeof block.apiVersion !== "number" || !Number.isInteger(block.apiVersion)) return { error: "`subshell.apiVersion` must be an integer" };
35
- if (block.apiVersion < 1) return { error: `this plugin declares plugin-api ${block.apiVersion}, which is not a version; the lowest is 1 and this host implements 1` };
36
- if (block.apiVersion > 1) return { error: `this plugin needs plugin-api ${block.apiVersion}, but this host implements 1; upgrade the agent` };
46
+ if (block.apiVersion < 1) return { error: `this plugin declares plugin-api ${block.apiVersion}, which is not a version; the lowest is 1 and this host implements 2` };
47
+ if (block.apiVersion > 2) return { error: `this plugin needs plugin-api ${block.apiVersion}, but this host implements 2; upgrade the agent` };
37
48
  if (typeof block.id !== "string" || !ID_RE.test(block.id)) return { error: "`subshell.id` must be lowercase letters, digits and hyphens, starting alphanumeric (it becomes a directory name)" };
38
49
  if (typeof block.type !== "string" || !PLUGIN_TYPES.includes(block.type)) return { error: `\`subshell.type\` must be one of: ${PLUGIN_TYPES.join(", ")}` };
39
50
  if (typeof block.name !== "string" || block.name.trim() === "") return { error: "`subshell.name` must be a non-empty string" };
40
51
  if (typeof block.description !== "string") return { error: "`subshell.description` must be a string" };
41
- if (block.icon !== void 0 && typeof block.icon !== "string") return { error: "`subshell.icon` must be a string" };
52
+ if (block.icon !== void 0) {
53
+ if (typeof block.icon !== "string" || block.icon.trim() === "") return { error: "`subshell.icon` must be a non-empty relative path to an image in the package" };
54
+ if (block.icon.startsWith("/") || block.icon.split("/").includes("..")) return { error: "`subshell.icon` must stay inside the package (no leading `/` and no `..` segment)" };
55
+ const icon = block.icon;
56
+ if (!ICON_EXTENSIONS.some((ext) => icon.endsWith(ext))) return { error: `\`subshell.icon\` must end in one of: ${ICON_EXTENSIONS.join(", ")}` };
57
+ }
42
58
  if (typeof block.entry !== "string" || block.entry.trim() === "") return { error: "`subshell.entry` must be a non-empty relative path" };
43
59
  if (block.entry.startsWith("/") || block.entry.split("/").includes("..")) return { error: "`subshell.entry` must stay inside the package (no leading `/` and no `..` segment)" };
44
60
  let detect;
@@ -81,23 +97,23 @@ function parseManifest(pkgJson) {
81
97
  //#endregion
82
98
  //#region ../../plugin-api/dist/index.js
83
99
  /**
84
- * The profile checks every built-in harness performs unchanged: a non-empty
100
+ * The preset checks every built-in harness performs unchanged: a non-empty
85
101
  * name, string env values, and flags that look like flags. Harnesses with
86
102
  * extra rules run their own checks on top of these.
87
- * @param profile - The decoded profile definition to check
103
+ * @param preset - The decoded preset definition to check
88
104
  * @returns Issues per field; `valid` is true when no issues were found
89
105
  */
90
- function validateGenericProfile(profile) {
106
+ function validateGenericPreset(preset) {
91
107
  const issues = [];
92
- if (!profile.name || profile.name.trim().length === 0) issues.push({
108
+ if (!preset.name || preset.name.trim().length === 0) issues.push({
93
109
  field: "name",
94
- message: "Profile name is required."
110
+ message: "Preset name is required."
95
111
  });
96
- for (const [key, value] of Object.entries(profile.env)) if (typeof value !== "string") issues.push({
112
+ for (const [key, value] of Object.entries(preset.env)) if (typeof value !== "string") issues.push({
97
113
  field: "env",
98
114
  message: `Env var "${key}" must be a string.`
99
115
  });
100
- for (const flag of profile.flags) if (!flag.startsWith("-")) issues.push({
116
+ for (const flag of preset.flags) if (!flag.startsWith("-")) issues.push({
101
117
  field: "flags",
102
118
  message: `Flag "${flag}" must start with "-".`
103
119
  });
@@ -119,7 +135,7 @@ function validateGenericProfile(profile) {
119
135
  const parsed = parseManifest({
120
136
  name: "@subshell-ai/plugin-claude-code",
121
137
  description: "Claude Code harness plugin for Subshell",
122
- version: "1.0.0",
138
+ version: "2.0.0",
123
139
  license: "Apache-2.0",
124
140
  type: "module",
125
141
  main: "dist/index.js",
@@ -128,6 +144,13 @@ const parsed = parseManifest({
128
144
  "import": "./dist/index.js"
129
145
  } },
130
146
  types: "dist/index.d.ts",
147
+ homepage: "https://subshell.sh",
148
+ repository: {
149
+ "type": "git",
150
+ "url": "git+https://github.com/subshell-ai/subshell.git",
151
+ "directory": "packages/plugins/claude-code"
152
+ },
153
+ bugs: { "url": "https://github.com/subshell-ai/subshell/issues" },
131
154
  scripts: {
132
155
  "build": "tsdown",
133
156
  "build:dev": "hash-runner",
@@ -146,15 +169,15 @@ const parsed = parseManifest({
146
169
  "tsdown": "0.22.14",
147
170
  "typescript": "7.0.2"
148
171
  },
149
- files: ["dist"],
172
+ files: ["dist", "icon.svg"],
150
173
  publishConfig: { "access": "public" },
151
174
  subshell: {
152
- "apiVersion": 1,
175
+ "apiVersion": 2,
153
176
  "id": "claude-code",
154
177
  "type": "agent-harness",
155
178
  "name": "Claude Code",
156
179
  "description": "Anthropic's agentic coding assistant (interactive CLI)",
157
- "icon": "🤖",
180
+ "icon": "icon.svg",
158
181
  "entry": "dist/index.js",
159
182
  "detect": {
160
183
  "binaryName": "claude",
@@ -345,53 +368,56 @@ const SUGGESTED_FLAGS = [
345
368
  }
346
369
  ];
347
370
  /**
348
- * Fire-and-forget attention reporting. Each hook runs `bun -e` (bun is on the
349
- * pane PATH — the image ships it, and no curl exists there) and POSTs the
350
- * subshell's own bearer to the attention endpoint; the server gates delivery
351
- * on the subshell's bell and derives the "waiting for you" state. The env
352
- * vars are baked into the pane by the backend (`subshellMcpEnv`), and every
353
- * failure path is swallowed: a missing hook event costs one notification,
354
- * never a broken subshell turn. SUBSHELL_BASE_URL must be reachable from inside
355
- * the pane's network — in the container that means the published port, which
356
- * compose already passes via APP_BASE_URL.
371
+ * One hook command line: the host-resolved reporter, then this hook's verb
372
+ * words, every element POSIX-quoted.
373
+ *
374
+ * A hook runs on the PANE's machine, so the command has to name something that
375
+ * exists there. It used to be `bun -e '<inlined JS>'` — bun was on the pane
376
+ * PATH because the container image shipped it, and no curl was there to use
377
+ * instead. That assumption did not survive leaving the container: on a desktop
378
+ * install the machine has the subshell binary and nothing else, and Claude Code
379
+ * opened every session with `/bin/sh: bun: command not found` while
380
+ * notifications and conversation identity silently never worked.
381
+ *
382
+ * So the reporting moved INTO the binary (`<self> report …`), and what the
383
+ * plugin composes is an argv rather than a program. Bounds, credentials and
384
+ * which fields travel are that subcommand's contract now, which is also why
385
+ * this function no longer needs to know the pane env exists.
357
386
  */
358
- 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))'`;
387
+ const reporterHook = (host, reporter, ...verb) => [
388
+ reporter.command,
389
+ ...reporter.args,
390
+ ...verb
391
+ ].map((word) => host.shellQuote(word)).join(" ");
359
392
  /**
360
- * Conversation-identity reporting. The restart-resume pin (`--session-id`
361
- * at launch) only survives while the pane keeps that ONE conversation — but
362
- * /clear, /resume <other>, and /fork start a DIFFERENT transcript id in-pane
363
- * and nothing else tells the server, so the next restart would resurrect a
364
- * stale conversation (observed 2026-09-03). The SessionStart hook fires on
365
- * every such transition (source: startup|resume|clear|compact|fork).
366
- *
367
- * Unlike the attention pings, THIS command reads stdin — the deliberate,
368
- * documented exception to the no-stdin rule (whose rationale is that
369
- * Stop/Notification payloads carry conversation text): the SessionStart
370
- * payload is pure metadata ({session_id, transcript_path, cwd, source,…})
371
- * and ONLY `session_id` is forwarded; the response is fire-and-forget like
372
- * the pings — every failure path is swallowed.
393
+ * The `--settings` hooks object for one launch.
373
394
  *
374
- * Both awaits are bounded: a self-kill timer caps the WHOLE hook (4 s — an
375
- * unclosed stdin must never stall the pane start, and a SessionStart hook
376
- * blocks the harness until it exits) and a 2 s fetch timeout caps the POST
377
- * (a lost report is harmless — the next transition re-reports).
395
+ * - `Stop` / `Notification` — fire-and-forget attention reporting. The server
396
+ * gates delivery on the subshell's bell and derives the "waiting for you"
397
+ * state; a missed event costs one notification, never a broken turn.
398
+ * - `SessionStart` — conversation identity. The restart-resume pin
399
+ * (`--session-id` at launch) only survives while the pane keeps that ONE
400
+ * conversation, but /clear, /resume <other> and /fork start a DIFFERENT
401
+ * transcript id in-pane and nothing else tells the server, so the next
402
+ * restart would resurrect a stale conversation (observed 2026-09-03). This
403
+ * fires on every such transition (source: startup|resume|clear|compact|fork)
404
+ * and is the one hook that reads stdin — the reporter forwards `session_id`
405
+ * from that payload and nothing else.
378
406
  */
379
- 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)'";
380
- /** The `--settings` hooks object injected into every Claude Code launch. */
381
- const ATTENTION_HOOKS = {
407
+ const attentionHooks = (host, reporter) => ({
382
408
  Stop: [{ hooks: [{
383
409
  type: "command",
384
- command: attentionPing("turn_complete")
410
+ command: reporterHook(host, reporter, "attention", "turn_complete")
385
411
  }] }],
386
412
  Notification: [{ hooks: [{
387
413
  type: "command",
388
- command: attentionPing("needs_attention")
414
+ command: reporterHook(host, reporter, "attention", "needs_attention")
389
415
  }] }],
390
416
  SessionStart: [{ hooks: [{
391
417
  type: "command",
392
- command: sessionReportPing()
418
+ command: reporterHook(host, reporter, "session")
393
419
  }] }]
394
- };
420
+ });
395
421
  /** Claude's transcript folder name for a project dir: every non-alphanumeric → `-`. */
396
422
  function projectSlug(cwd) {
397
423
  return cwd.replace(/[^a-zA-Z0-9]/g, "-");
@@ -400,9 +426,9 @@ function projectSlug(cwd) {
400
426
  * Built-in harness: Claude Code.
401
427
  *
402
428
  * Launch shape:
403
- * claude --settings <json> --name <subshell> [profile flags] [extra flags]
429
+ * claude --settings <json> --name <subshell> [preset flags] [extra flags]
404
430
  * Settings are passed as a JSON string via `--settings` (per-invocation, so
405
- * profile config never needs to write into the user's real ~/.claude).
431
+ * preset config never needs to write into the user's real ~/.claude).
406
432
  *
407
433
  * Identity, detection and install guidance live in this package's
408
434
  * package.json `subshell` block, NOT here: the host reads them without
@@ -413,7 +439,7 @@ function projectSlug(cwd) {
413
439
  * version probe, shell quoting, a namespaced logger). See
414
440
  * `@subshell-ai/plugin-api`.
415
441
  */
416
- const createPlugin = (_host) => ({
442
+ const createPlugin = (host) => ({
417
443
  capabilities: () => [
418
444
  "mcp",
419
445
  "resume",
@@ -446,17 +472,16 @@ const createPlugin = (_host) => ({
446
472
  }
447
473
  },
448
474
  buildCommand(input) {
449
- const { binary, profile, subshellName, extraFlags, mcp, harnessSession } = input;
475
+ const { binary, preset, subshellName, extraFlags, mcp, harnessSession, reporter } = input;
450
476
  const args = [binary];
451
477
  if (mcp?.args) args.push(...mcp.args);
452
478
  if (harnessSession) args.push(harnessSession.mode === "resume" ? "--resume" : "--session-id", harnessSession.id);
453
- const settings = {
454
- ...profile.settings ?? {},
455
- hooks: ATTENTION_HOOKS
456
- };
457
- args.push("--settings", JSON.stringify(settings));
479
+ const settings = { ...preset.settings ?? {} };
480
+ if (reporter) settings.hooks = attentionHooks(host, reporter);
481
+ else host.log.warn("no reporter resolved for this launch: attention and conversation-identity hooks are omitted");
482
+ if (Object.keys(settings).length > 0) args.push("--settings", JSON.stringify(settings));
458
483
  if (subshellName) args.push("--name", subshellName);
459
- for (const flag of profile.flags) args.push(flag);
484
+ for (const flag of preset.flags) args.push(flag);
460
485
  if (extraFlags) args.push(...extraFlags);
461
486
  return args;
462
487
  },
@@ -477,10 +502,10 @@ const createPlugin = (_host) => ({
477
502
  summary: "Subshell registers itself with every Claude Code subshell automatically (via --mcp-config)."
478
503
  };
479
504
  },
480
- validateProfile(profile) {
481
- return validateGenericProfile(profile);
505
+ validatePreset(preset) {
506
+ return validateGenericPreset(preset);
482
507
  },
483
- profileSettings() {
508
+ presetSettings() {
484
509
  return CLAUDE_SETTINGS_FIELDS;
485
510
  },
486
511
  suggestedEnv() {
@@ -499,4 +524,4 @@ const createPlugin = (_host) => ({
499
524
  }
500
525
  });
501
526
  //#endregion
502
- export { ATTENTION_HOOKS, createPlugin as default, manifest };
527
+ export { createPlugin as default, manifest };
package/icon.svg ADDED
@@ -0,0 +1,3 @@
1
+ <svg viewBox="0 0 248 248" fill="none" xmlns="http://www.w3.org/2000/svg"><title>Claude Code</title>
2
+ <path d="M52.4285 162.873L98.7844 136.879L99.5485 134.602L98.7844 133.334H96.4921L88.7237 132.862L62.2346 132.153L39.3113 131.207L17.0249 130.026L11.4214 128.844L6.2 121.873L6.7094 118.447L11.4214 115.257L18.171 115.847L33.0711 116.911L55.485 118.447L71.6586 119.392L95.728 121.873H99.5485L100.058 120.337L98.7844 119.392L97.7656 118.447L74.5877 102.732L49.4995 86.1905L36.3823 76.62L29.3779 71.7757L25.8121 67.2858L24.2839 57.3608L30.6515 50.2716L39.3113 50.8623L41.4763 51.4531L50.2636 58.1879L68.9842 72.7209L93.4357 90.6804L97.0015 93.6343L98.4374 92.6652L98.6571 91.9801L97.0015 89.2625L83.757 65.2772L69.621 40.8192L63.2534 30.6579L61.5978 24.632C60.9565 22.1032 60.579 20.0111 60.579 17.4246L67.8381 7.49965L71.9133 6.19995L81.7193 7.49965L85.7946 11.0443L91.9074 24.9865L101.714 46.8451L116.996 76.62L121.453 85.4816L123.873 93.6343L124.764 96.1155H126.292V94.6976L127.566 77.9197L129.858 57.3608L132.15 30.8942L132.915 23.4505L136.608 14.4708L143.994 9.62643L149.725 12.344L154.437 19.0788L153.8 23.4505L150.998 41.6463L145.522 70.1215L141.957 89.2625H143.994L146.414 86.7813L156.093 74.0206L172.266 53.698L179.398 45.6635L187.803 36.802L193.152 32.5484H203.34L210.726 43.6549L207.415 55.1159L196.972 68.3492L188.312 79.5739L175.896 96.2095L168.191 109.585L168.882 110.689L170.738 110.53L198.755 104.504L213.91 101.787L231.994 98.7149L240.144 102.496L241.036 106.395L237.852 114.311L218.495 119.037L195.826 123.645L162.07 131.592L161.696 131.893L162.137 132.547L177.36 133.925L183.855 134.279H199.774L229.447 136.524L237.215 141.605L241.8 147.867L241.036 152.711L229.065 158.737L213.019 154.956L175.45 145.977L162.587 142.787H160.805V143.85L171.502 154.366L191.242 172.089L215.82 195.011L217.094 200.682L213.91 205.172L210.599 204.699L188.949 188.394L180.544 181.069L161.696 165.118H160.422V166.772L164.752 173.152L187.803 207.771L188.949 218.405L187.294 221.832L181.308 223.959L174.813 222.777L161.187 203.754L147.305 182.486L136.098 163.345L134.745 164.2L128.075 235.42L125.019 239.082L117.887 241.8L111.902 237.31L108.718 229.984L111.902 215.452L115.722 196.547L118.779 181.541L121.58 162.873L123.291 156.636L123.14 156.219L121.773 156.449L107.699 175.752L86.304 204.699L69.3663 222.777L65.291 224.431L58.2867 220.768L58.9235 214.27L62.8713 208.48L86.304 178.705L100.44 160.155L109.551 149.507L109.462 147.967L108.959 147.924L46.6977 188.512L35.6182 189.93L30.7788 185.44L31.4156 178.115L33.7079 175.752L52.4285 162.873Z" fill="#D97757"/>
3
+ </svg>
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": "1.0.0",
4
+ "version": "2.0.0",
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",
@@ -30,19 +39,17 @@
30
39
  "tsdown": "0.22.14",
31
40
  "typescript": "7.0.2"
32
41
  },
33
- "files": [
34
- "dist"
35
- ],
42
+ "files": ["dist", "icon.svg"],
36
43
  "publishConfig": {
37
44
  "access": "public"
38
45
  },
39
46
  "subshell": {
40
- "apiVersion": 1,
47
+ "apiVersion": 2,
41
48
  "id": "claude-code",
42
49
  "type": "agent-harness",
43
50
  "name": "Claude Code",
44
51
  "description": "Anthropic's agentic coding assistant (interactive CLI)",
45
- "icon": "\ud83e\udd16",
52
+ "icon": "icon.svg",
46
53
  "entry": "dist/index.js",
47
54
  "detect": {
48
55
  "binaryName": "claude",
@@ -54,7 +61,9 @@
54
61
  ".npm-global/bin/claude"
55
62
  ]
56
63
  },
57
- "hostEnv": ["CLAUDE_CONFIG_DIR"],
64
+ "hostEnv": [
65
+ "CLAUDE_CONFIG_DIR"
66
+ ],
58
67
  "install": {
59
68
  "command": "curl -fsSL https://claude.ai/install.sh | bash",
60
69
  "docsUrl": "https://code.claude.com/docs/en/setup"