expandai 0.0.3 → 0.0.4

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.
@@ -1,6 +1,5 @@
1
1
  import { homedir } from 'node:os';
2
- import { Array, Config, Console, Effect, Match, Schema } from 'effect';
3
- import { CLI_VERSION } from '../../Version.js';
2
+ import { Config, Console, Effect, Match, Schema } from 'effect';
4
3
  import { ConfigIo, cliShellCommand, DIRECT_CLI } from '../Domain.js';
5
4
  /** Claude Code WebFetch hook invocation persisted in harness settings. */
6
5
  export const HOOK_INVOCATION = 'expandai hook claude-code';
@@ -41,178 +40,35 @@ export function isExpandaiHookCommand(command) {
41
40
  return direct.test(value) || absolute.test(value) || shellFallback.test(value);
42
41
  }
43
42
  /**
44
- * `expandaiManagedDeny` records whether *we* added `WebFetch` to
45
- * `permissions.deny`, so uninstall can tell our entry from an identical one the
46
- * user set first. A deny entry is a bare string with nowhere to carry that, and
47
- * "our hook is present" is not the same claim: installing over a settings file
48
- * that already denied `WebFetch` leaves the hook without us having written the
49
- * rule, and removing it then revokes a control its owner chose.
50
- */
51
- export function makeExpandaiMatcher(profileVersion, cli = DIRECT_CLI, managedDeny = true) {
52
- return {
53
- matcher: WEBFETCH_MATCHER,
54
- hooks: [
55
- {
56
- type: 'command',
57
- command: hookCommandFor(cli),
58
- expandaiProfileVersion: profileVersion,
59
- expandaiManagedDeny: managedDeny,
60
- },
61
- ],
62
- };
63
- }
64
- /** True only when our own hook entry says we wrote the deny rule. */
65
- export function expandaiOwnsWebFetchDeny(settings) {
66
- return (settings.hooks.PreToolUse ?? []).some((entry) => entry.hooks.some((hook) => isExpandaiHookCommand(hook.command) && hook.expandaiManagedDeny === true));
67
- }
68
- function webFetchAlreadyDenied(settings) {
69
- return (settings.permissions?.deny ?? []).includes(WEBFETCH_MATCHER);
70
- }
71
- export const expandaiMatcher = makeExpandaiMatcher(CLI_VERSION);
72
- function stripExpandaiHooks(entries) {
73
- return Array.flatMap(entries, (entry) => {
74
- const hooks = Array.filter(entry.hooks, (hook) => !isExpandaiHookCommand(hook.command));
75
- return hooks.length === 0 && hooks.length !== entry.hooks.length ? [] : [{ ...entry, hooks }];
76
- });
77
- }
78
- /**
79
- * Denying `WebFetch` removes it from the toolset rather than blocking it per call,
80
- * so the agent reaches for Expand's tool instead of being refused. That difference
81
- * is the whole point: a hook can only answer a WebFetch call with `deny`, which
82
- * Claude Code renders to the user as `Error:` on every single fetch.
43
+ * Nothing here writes to `settings.json`, and nothing here removes from it.
83
44
  *
84
- * The hook stays installed as the fallback. With the deny rule in force WebFetch is
85
- * never called, so the hook never fires; if the rule is ever absent or overridden,
86
- * the hook still intercepts — with the red line, but correctly.
45
+ * A `WebFetch` deny entry is a bare string with nowhere to record who wrote it,
46
+ * and our hook is not proof either: `hook install` used to write the hook on its
47
+ * own, so a settings file carrying our hook and a `WebFetch` deny may well hold a
48
+ * rule the user added afterwards. Removing it would revoke a restriction its
49
+ * owner chose. Since Claude Code no longer gets a hook or a deny rule from us,
50
+ * there is nothing of ours left to clean up, and guessing is the only thing a
51
+ * cleanup pass could do.
87
52
  */
88
- export function planClaudeWebFetchDeny(settings) {
89
- const deny = settings.permissions?.deny ?? [];
90
- if (deny.includes(WEBFETCH_MATCHER))
91
- return { changed: false, settings };
92
- return {
93
- changed: true,
94
- settings: {
95
- ...settings,
96
- permissions: { ...(settings.permissions ?? {}), deny: [...deny, WEBFETCH_MATCHER] },
97
- },
98
- };
99
- }
100
- /**
101
- * Drops the `WebFetch` entry; anything else the user denied is left alone.
102
- *
103
- * A deny entry is a bare string with nowhere to record who wrote it, so this
104
- * cannot tell our rule from an identical one the user added themselves. The
105
- * caller supplies that evidence — see `uninstall`.
106
- */
107
- export function planClaudeWebFetchDenyRemoval(settings) {
108
- const deny = settings.permissions?.deny;
109
- if (deny === undefined || !deny.includes(WEBFETCH_MATCHER))
110
- return { changed: false, settings };
111
- const remaining = deny.filter((entry) => entry !== WEBFETCH_MATCHER);
112
- const permissions = { ...settings.permissions, deny: remaining };
113
- return { changed: true, settings: { ...settings, permissions } };
114
- }
115
- export function planClaudeHook(settings, profileVersion = CLI_VERSION, cli = DIRECT_CLI) {
116
- const preToolUse = settings.hooks.PreToolUse ?? [];
117
- const owned = preToolUse.flatMap((entry) => entry.hooks.filter((hook) => isExpandaiHookCommand(hook.command)).map((hook) => ({ entry, hook })));
118
- // The marker records history — did *this* install add the rule — so once it is
119
- // written it must be preserved, not recomputed. Recomputing it from present
120
- // state makes the plan disagree with what it just wrote: after we add the deny
121
- // rule, `webFetchAlreadyDenied` is true, so a second plan would decide the rule
122
- // was never ours and rewrite the hook forever.
123
- const existingMarker = owned[0]?.hook.expandaiManagedDeny;
124
- const managedDeny = existingMarker ?? !webFetchAlreadyDenied(settings);
125
- const current = owned.length === 1 &&
126
- owned[0]?.entry.matcher === WEBFETCH_MATCHER &&
127
- owned[0]?.hook.command === hookCommandFor(cli) &&
128
- owned[0]?.hook.expandaiProfileVersion === profileVersion &&
129
- owned[0]?.hook.expandaiManagedDeny === managedDeny;
130
- if (current)
131
- return { changed: false, settings };
132
- return {
133
- changed: true,
134
- settings: {
135
- ...settings,
136
- hooks: {
137
- ...settings.hooks,
138
- PreToolUse: [...stripExpandaiHooks(preToolUse), makeExpandaiMatcher(profileVersion, cli, managedDeny)],
139
- },
140
- },
141
- };
142
- }
143
- export function planClaudeHookRemoval(settings) {
144
- const preToolUse = settings.hooks.PreToolUse ?? [];
145
- const changed = preToolUse.some((entry) => entry.hooks.some((hook) => isExpandaiHookCommand(hook.command)));
146
- return changed
147
- ? {
148
- changed,
149
- settings: {
150
- ...settings,
151
- hooks: { ...settings.hooks, PreToolUse: stripExpandaiHooks(preToolUse) },
152
- },
153
- }
154
- : { changed, settings };
155
- }
156
53
  export class ClaudeHook extends Effect.Service()('@expandai/cli/Install/Claude/Hook', {
157
54
  dependencies: [ConfigIo.Default],
158
55
  effect: Effect.gen(function* () {
159
- const io = yield* ConfigIo;
160
56
  const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()));
161
57
  const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()));
162
58
  const pathFor = Effect.fn(function* (scope) {
163
59
  return yield* Match.value(scope).pipe(Match.when('global', () => Effect.succeed(`${home}/.claude/settings.json`)), Match.when('project', () => Effect.succeed(`${cwd}/.claude/settings.json`)), Match.exhaustive);
164
60
  });
165
- const install = Effect.fn('ClaudeHook.install')(function* (scope) {
166
- const target = yield* pathFor(scope);
167
- const settings = yield* io.read(target, SettingsSchema);
168
- // Both halves, exactly as `init` writes them. This used to install the hook
169
- // alone, which left the deny rule to `init` only — and made `uninstall`'s
170
- // ownership rule false: it treats our hook as evidence that we wrote the
171
- // deny entry, so a rule the user added after a bare `hook install` was
172
- // deleted as if it were ours.
173
- const hookPlan = planClaudeHook(settings);
174
- const plan = planClaudeWebFetchDeny(hookPlan.settings);
175
- if (!hookPlan.changed && !plan.changed) {
176
- yield* Console.log(`expandai WebFetch hook already installed at ${target}`);
177
- return;
178
- }
179
- yield* io.write(target, plan.settings, SettingsSchema);
180
- yield* Console.log(`Installed expandai WebFetch hook and deny rule at ${target}`);
61
+ const NOTICE = 'expandai does not install a WebFetch hook for Claude Code — the MCP server and runtime skill replace it, and neither can disable a built-in tool.';
62
+ /**
63
+ * Kept as a no-op rather than deleted so `hook install|uninstall claude-code`
64
+ * still resolves. Both say what the profile is instead of touching
65
+ * `settings.json`, which expandai no longer writes for Claude Code.
66
+ */
67
+ const install = Effect.fn('ClaudeHook.install')(function* (_scope) {
68
+ yield* Console.log(NOTICE);
181
69
  });
182
- const uninstall = Effect.fn('ClaudeHook.uninstall')(function* (scope) {
183
- const target = yield* pathFor(scope);
184
- if (!(yield* io.exists(target)))
185
- return;
186
- const settings = yield* io.read(target, SettingsSchema);
187
- // Uninstall has to undo both halves, or `WebFetch` stays denied with nothing
188
- // left to serve the fetch — worse than never having installed us.
189
- //
190
- // But only the halves we wrote. `expandaiManagedDeny` on our hook records
191
- // whether this install added the deny rule; without it, the rule is either
192
- // the user's own or from an install predating the marker, and those are
193
- // indistinguishable. Guessing "ours" revokes a control its owner chose;
194
- // guessing "theirs" leaves `WebFetch` denied with nothing serving fetches.
195
- // So leave it and say so — `uninstall` also fans out over both scopes, and
196
- // would otherwise reach into a global settings file a project-scoped
197
- // install never touched.
198
- const hookPlan = planClaudeHookRemoval(settings);
199
- if (!hookPlan.changed)
200
- return;
201
- // Read the marker off the hook before it is stripped.
202
- const owned = expandaiOwnsWebFetchDeny(settings);
203
- const denyPlan = owned ? planClaudeWebFetchDenyRemoval(hookPlan.settings) : hookPlan;
204
- const strandedDeny = !owned && (settings.permissions?.deny ?? []).includes(WEBFETCH_MATCHER);
205
- yield* io.write(target, denyPlan.settings, SettingsSchema);
206
- // `denyPlan` is the hook plan itself when we do not own the rule, so its
207
- // `changed` flag reports the hook removal — not a deny removal that never
208
- // happened.
209
- const removedDeny = owned && denyPlan.changed;
210
- yield* Console.log(removedDeny
211
- ? `Removed expandai WebFetch hook and deny rule from ${target}`
212
- : `Removed expandai WebFetch hook from ${target}`);
213
- if (strandedDeny) {
214
- yield* Console.log(`Left "WebFetch" in permissions.deny at ${target} — expandai could not confirm it added that rule. Remove it by hand if you want Claude Code's built-in WebFetch back.`);
215
- }
70
+ const uninstall = Effect.fn('ClaudeHook.uninstall')(function* (_scope) {
71
+ yield* Console.log(NOTICE);
216
72
  });
217
73
  return { install, uninstall, pathFor };
218
74
  }),
@@ -1 +1 @@
1
- {"version":3,"file":"Hook.js","sourceRoot":"","sources":["../../../../src/Install/Claude/Hook.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"Hook.js","sourceRoot":"","sources":["../../../../src/Install/Claude/Hook.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  // Single source of truth for the CLI version. Keep in sync with package.json —
2
2
  // the compiled binary cannot read package.json at runtime.
3
- export const CLI_VERSION = '0.0.3';
3
+ export const CLI_VERSION = '0.0.4';
4
4
  //# sourceMappingURL=Version.js.map
@@ -1,12 +1,14 @@
1
1
  ---
2
2
  name: expandai
3
- description: Fetch any URL as clean markdown using the `expandai` CLI. Use when reading documentation, articles, GitHub READMEs, blog posts, or any web page — the output is rendered from the live page so it handles SPAs and bot-protected sites that plain `curl` does not.
3
+ description: Use when any public http(s) URL needs to be read — documentation, articles, GitHub pages, blog posts, release notes, or a link the user pasted — and before calling WebFetch or curl on it. WebFetch does not execute JavaScript, so also use this whenever a fetch came back empty, truncated, 403, paywalled, or asking you to enable JavaScript. Quoting a captured page requires the citation protocol below. Not for localhost, private or internal hosts, or JSON API endpoints.
4
4
  metadata:
5
5
  expandai-profile-version: 0.0.2
6
6
  ---
7
7
 
8
8
  # expandai
9
9
 
10
+ **Before answering from a page you fetched, read the Citations section below.**
11
+
10
12
  Fetch any URL as clean markdown.
11
13
 
12
14
  ```bash
@@ -15,7 +17,12 @@ expandai fetch <url>
15
17
 
16
18
  ## When to use
17
19
 
18
- Use `expandai fetch` instead of `curl` or `WebFetch` when you need to read web content. It renders the live page (handles JavaScript, SPAs, bot protection) and returns markdown rather than raw HTML.
20
+ Reach for `expandai fetch` before `curl` or `WebFetch`. It renders the live page in a browser —
21
+ JavaScript, SPAs, bot protection — and returns markdown rather than raw HTML. `WebFetch` has no
22
+ JavaScript engine, so on a client-rendered page it returns the empty shell and nothing else.
23
+
24
+ Skip it for localhost, private or internal hosts, and JSON API endpoints; `curl` is the right
25
+ tool there.
19
26
 
20
27
  ## Usage
21
28
 
@@ -33,9 +40,8 @@ expandai fetch https://example.com --format json # object-mode { meta, markdown
33
40
 
34
41
  ## Search a page you already fetched
35
42
 
36
- Every fetch produces a snapshot handle (`meta.snapshotId`). When the WebFetch hook serves a
37
- page, it injects that handle as a note at the top of the markdown. Once a page is captured,
38
- extract specific passages by **searching the snapshot by handle** — don't fetch the URL again:
43
+ Every fetch produces a snapshot handle (`meta.snapshotId`). Once a page is captured, extract
44
+ specific passages by **searching the snapshot by handle** — don't fetch the URL again:
39
45
 
40
46
  ```bash
41
47
  expandai search <snapshotId> "your query" # snippets from the stored snapshot
@@ -52,7 +58,7 @@ prefer `expandai search` (or `fetch_search`), which reuses the existing capture.
52
58
  ## Citations
53
59
 
54
60
  Cite every claim you take from a fetched page. This is not optional, and it applies however
55
- the page arrived — `expandai fetch`, `expandai search`, the MCP tools, or the WebFetch hook.
61
+ the page arrived — `expandai fetch`, `expandai search`, or the MCP tools.
56
62
 
57
63
  Captured markdown stamps an evidence id next to the text it belongs to, written as `{1199}`.
58
64
  For each sentence you use, take the id nearest that text and end your answer with:
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "expandai",
3
3
  "type": "module",
4
- "version": "0.0.3",
4
+ "version": "0.0.4",
5
5
  "private": false,
6
6
  "description": "Unified CLI and MCP server for ExpandAI",
7
7
  "author": "ExpandAI",
@@ -2,14 +2,7 @@ import { homedir } from 'node:os'
2
2
  import { FileSystem, Path } from '@effect/platform'
3
3
  import { NodeFileSystem, NodePath } from '@effect/platform-node'
4
4
  import { Array, Config, Effect, Either, Layer, Option, Schema } from 'effect'
5
- import {
6
- hookCommandFor,
7
- isExpandaiHookCommand,
8
- planClaudeHook,
9
- planClaudeWebFetchDeny,
10
- SettingsSchema,
11
- WEBFETCH_MATCHER,
12
- } from '../Install/Claude/Hook.js'
5
+ import { SettingsSchema } from '../Install/Claude/Hook.js'
13
6
  import { planClaudeMcp } from '../Install/Claude/Mcp.js'
14
7
  import { planClaudeSkill, SKILL_FILE_NAME, SKILL_NAME } from '../Install/Claude/Skill.js'
15
8
  import { type CliCommand, CliInvocation, McpServersConfigSchema, SERVER_KEY } from '../Install/Domain.js'
@@ -19,7 +12,6 @@ import type { MutationExpectedState, MutationOperation } from './MutationTransac
19
12
 
20
13
  export const CLAUDE_PROFILE_VERSION = CLI_VERSION
21
14
 
22
- export const CLAUDE_POLICY_MANAGED_HOOKS = 'managed_hooks_only'
23
15
  export const CLAUDE_POLICY_MANAGED_MCP = 'managed_mcp_only'
24
16
  export const CLAUDE_POLICY_MCP_DENIED = 'managed_mcp_denied'
25
17
  export const CLAUDE_POLICY_MCP_NOT_ALLOWED = 'managed_mcp_not_allowed'
@@ -145,8 +137,10 @@ function managedPolicyBlocks(settings: ReadonlyArray<Record<string, unknown>>):
145
137
  const managedMcpOnly = settings.some((value) => value.allowManagedMcpServersOnly === true)
146
138
  const allowedMcpConfigured = settings.some((value) => Array.isArray(value.allowedMcpServers))
147
139
  const expandaiAllowed = settings.some((value) => listContainsExpandai(value.allowedMcpServers))
140
+ // `allowManagedHooksOnly` is deliberately not consulted. It restricts which hooks
141
+ // may be installed, and this profile installs none — blocking on it would refuse
142
+ // a skill-plus-MCP setup the policy has no objection to.
148
143
  for (const value of settings) {
149
- if (value.allowManagedHooksOnly === true) blocks.add(CLAUDE_POLICY_MANAGED_HOOKS)
150
144
  if (listContainsExpandai(value.deniedMcpServers)) blocks.add(CLAUDE_POLICY_MCP_DENIED)
151
145
  }
152
146
  if (managedMcpOnly && !expandaiAllowed) blocks.add(CLAUDE_POLICY_MANAGED_MCP)
@@ -170,30 +164,7 @@ function mcpLegacyVariant(value: unknown): string[] {
170
164
  return ['owned-entry']
171
165
  }
172
166
 
173
- function hookLegacyVariant(command: string): string {
174
- if (/^npx\s+-y\s+/u.test(command.trim())) return 'npx-y'
175
- if (/\shook\s+claude\s*$/u.test(command)) return 'old-command-spelling'
176
- if (/^(?:"|'|\/|[A-Za-z]:\\)/u.test(command.trim())) return 'absolute-path'
177
- if (/^if\s+command\s+-v\s+expandai/u.test(command.trim())) return 'npx-shell-fallback'
178
- return 'unversioned'
179
- }
180
-
181
167
  function makeInspection(scope: InitScope, loaded: LoadedClaudeProfile, cli: CliCommand): ClaudeProfileInspection {
182
- const hookPlan = planClaudeHook(loaded.settings, CLAUDE_PROFILE_VERSION, cli)
183
- const denyPlan = planClaudeWebFetchDeny(hookPlan.settings)
184
- const hookEntries = (loaded.settings.hooks.PreToolUse ?? []).flatMap((entry) =>
185
- entry.hooks.filter((hook) => isExpandaiHookCommand(hook.command)).map((hook) => ({ entry, hook })),
186
- )
187
- const hookVersion = hookEntries.length === 1 ? hookEntries[0]?.hook.expandaiProfileVersion : undefined
188
- const hookCurrent = !hookPlan.changed && !denyPlan.changed
189
- const hookLegacy = hookCurrent
190
- ? []
191
- : hookEntries.map(({ entry, hook }) =>
192
- entry.matcher === WEBFETCH_MATCHER && hook.command === hookCommandFor(cli)
193
- ? 'unversioned'
194
- : hookLegacyVariant(hook.command),
195
- )
196
-
197
168
  const plannedSkill = planClaudeSkill(loaded.skillText, loaded.bundledSkillText, CLAUDE_PROFILE_VERSION)
198
169
  const currentSkillVersion = skillVersion(loaded.skillText)
199
170
  const skillCurrent = loaded.skillText !== undefined && !plannedSkill.changed
@@ -201,7 +172,6 @@ function makeInspection(scope: InitScope, loaded: LoadedClaudeProfile, cli: CliC
201
172
  const mcpValue = loaded.mcp.mcpServers[SERVER_KEY]
202
173
  const mcpCurrent = !planClaudeMcp(loaded.mcp, cli).changed
203
174
  const components: InitComponent[] = []
204
- if (hookCurrent) components.push('webfetch-hook')
205
175
  if (skillCurrent) components.push('runtime-skill')
206
176
  if (mcpCurrent) components.push('mcp')
207
177
 
@@ -211,12 +181,9 @@ function makeInspection(scope: InitScope, loaded: LoadedClaudeProfile, cli: CliC
211
181
  return {
212
182
  scope,
213
183
  targets: loaded.targets,
214
- hook: {
215
- current: hookCurrent,
216
- installed: hookEntries.length > 0,
217
- legacyVariants: hookLegacy,
218
- ...(hookVersion === undefined ? {} : { version: hookVersion }),
219
- },
184
+ // Claude Code has no hook component. Reported as absent-and-current so a
185
+ // caller that still reads this field cannot conclude something is missing.
186
+ hook: { current: true, installed: false, legacyVariants: [] },
220
187
  skill: {
221
188
  current: skillCurrent,
222
189
  installed: loaded.skillText !== undefined,
@@ -229,16 +196,15 @@ function makeInspection(scope: InitScope, loaded: LoadedClaudeProfile, cli: CliC
229
196
  legacyVariants: mcpCurrent ? [] : mcpLegacyVariant(mcpValue),
230
197
  },
231
198
  components,
232
- configured: components.length === 3,
199
+ configured: components.length === 2,
233
200
  profileVersion: CLAUDE_PROFILE_VERSION,
234
201
  componentVersions: {
235
- ...(hookVersion === undefined ? {} : { hook: hookVersion }),
236
202
  ...(currentSkillVersion === undefined ? {} : { skill: currentSkillVersion }),
237
203
  ...(mcpCurrent ? { mcp: CLAUDE_PROFILE_VERSION } : {}),
238
204
  },
239
205
  policyBlocks: [...new Set(policyBlocks)].sort(),
240
206
  remotePolicyMayApply: true,
241
- reloadMayBeRequired: !hookCurrent || !mcpCurrent,
207
+ reloadMayBeRequired: !mcpCurrent,
242
208
  consentMayBeRequired: scope === 'project',
243
209
  }
244
210
  }
@@ -383,7 +349,10 @@ const makeClaudeProfile = Effect.gen(function* () {
383
349
  ),
384
350
  ),
385
351
  )
386
- const readOnlyTargets = yield* Effect.filter([targets.settingsFile, targets.skillFile, targets.mcpFile], (target) =>
352
+ // Only the files this profile writes. `settings.json` is left alone, so its
353
+ // being read-only says nothing about whether setup can succeed — checking it
354
+ // would refuse an install that never touches it.
355
+ const readOnlyTargets = yield* Effect.filter([targets.skillFile, targets.mcpFile], (target) =>
387
356
  writableAncestor(target).pipe(Effect.map((writable) => !writable)),
388
357
  )
389
358
 
@@ -409,33 +378,23 @@ const makeClaudeProfile = Effect.gen(function* () {
409
378
 
410
379
  const plan = Effect.fn('ClaudeProfile.plan')(function* (scope: InitScope) {
411
380
  const loaded = yield* load(scope)
412
- const hookPlan = planClaudeHook(loaded.settings, CLAUDE_PROFILE_VERSION, cli)
413
- // One settings file, one write: the deny rule is layered onto the hook plan's
414
- // output so both land together or neither does.
415
- const denyPlan = planClaudeWebFetchDeny(hookPlan.settings)
416
- const settingsChanged = hookPlan.changed || denyPlan.changed
381
+ // The Claude Code profile is the MCP server plus the runtime skill, and nothing
382
+ // else. `settings.json` is not written at all: no `permissions` entry, no
383
+ // PreToolUse hook. Both of those can disable a built-in tool, and a install
384
+ // that can disable a built-in tool can also fail leaving it disabled.
417
385
  const skillPlan = planClaudeSkill(loaded.skillText, loaded.bundledSkillText, CLAUDE_PROFILE_VERSION)
418
386
  const mcpPlan = planClaudeMcp(loaded.mcp, cli)
419
- const settingsText = `${yield* Schema.encode(SettingsSchema)(denyPlan.settings)}\n`
420
387
  const mcpText = `${yield* Schema.encode(McpServersConfigSchema)(mcpPlan.config)}\n`
421
388
  const settingsDirectory = path.dirname(loaded.targets.settingsFile)
422
389
  const skillsDirectory = path.dirname(loaded.targets.skillDirectory)
423
390
  const operations: MutationOperation[] = []
424
- if (settingsChanged || skillPlan.changed) {
391
+ if (skillPlan.changed) {
425
392
  operations.push({
426
393
  type: 'mkdir',
427
394
  path: settingsDirectory,
428
395
  expected: yield* expectedDirectory(settingsDirectory),
429
396
  })
430
397
  }
431
- if (settingsChanged) {
432
- operations.push({
433
- type: 'write',
434
- path: loaded.targets.settingsFile,
435
- bytes: textEncoder.encode(settingsText),
436
- expected: loaded.settingsExpected,
437
- })
438
- }
439
398
  if (skillPlan.changed) {
440
399
  operations.push({
441
400
  type: 'mkdir',
@@ -471,8 +430,6 @@ const makeClaudeProfile = Effect.gen(function* () {
471
430
  scope,
472
431
  {
473
432
  ...inspectedLoaded,
474
- settings: denyPlan.settings,
475
- settingsText,
476
433
  skillText: skillPlan.text,
477
434
  mcp: mcpPlan.config,
478
435
  mcpText,
@@ -480,7 +437,7 @@ const makeClaudeProfile = Effect.gen(function* () {
480
437
  cli,
481
438
  )
482
439
  return {
483
- changed: settingsChanged || skillPlan.changed || mcpPlan.changed,
440
+ changed: skillPlan.changed || mcpPlan.changed,
484
441
  inspection,
485
442
  resultInspection,
486
443
  operations,
@@ -200,7 +200,7 @@ function isCompleteHarnessInspection(
200
200
  if (inspection.integrationVersion === undefined || inspection.integrationVersion.trim().length === 0) return false
201
201
  if (inspection.nextAction.trim().length === 0) return false
202
202
  const components = new Set(inspection.components)
203
- const expectsHook = harness === 'claude-code' || harness === 'opencode'
203
+ const expectsHook = harness === 'opencode'
204
204
  return (
205
205
  components.size === inspection.components.length &&
206
206
  components.has('runtime-skill') &&
@@ -152,7 +152,7 @@ function validateInspection(
152
152
  // Only the harnesses with a web-fetch hook install three components; cursor
153
153
  // and codex are complete with two. Demanding three of them reported a
154
154
  // correctly configured machine as an invalid result.
155
- const expectsHook = harness === 'claude-code' || harness === 'opencode'
155
+ const expectsHook = harness === 'opencode'
156
156
  const allComponents =
157
157
  componentSet.has('runtime-skill') &&
158
158
  componentSet.has('mcp') &&