/ skills / bundled / updateConfig.ts
updateConfig.ts
  1  import { toJSONSchema } from 'zod/v4'
  2  import { SettingsSchema } from '../../utils/settings/types.js'
  3  import { jsonStringify } from '../../utils/slowOperations.js'
  4  import { registerBundledSkill } from '../bundledSkills.js'
  5  
  6  /**
  7   * Generate JSON Schema from the settings Zod schema.
  8   * This keeps the skill prompt in sync with the actual types.
  9   */
 10  function generateSettingsSchema(): string {
 11    const jsonSchema = toJSONSchema(SettingsSchema(), { io: 'input' })
 12    return jsonStringify(jsonSchema, null, 2)
 13  }
 14  
 15  const SETTINGS_EXAMPLES_DOCS = `## Settings File Locations
 16  
 17  Choose the appropriate file based on scope:
 18  
 19  | File | Scope | Git | Use For |
 20  |------|-------|-----|---------|
 21  | \`~/.claude/settings.json\` | Global | N/A | Personal preferences for all projects |
 22  | \`.claude/settings.json\` | Project | Commit | Team-wide hooks, permissions, plugins |
 23  | \`.claude/settings.local.json\` | Project | Gitignore | Personal overrides for this project |
 24  
 25  Settings load in order: user → project → local (later overrides earlier).
 26  
 27  ## Settings Schema Reference
 28  
 29  ### Permissions
 30  \`\`\`json
 31  {
 32    "permissions": {
 33      "allow": ["Bash(npm:*)", "Edit(.claude)", "Read"],
 34      "deny": ["Bash(rm -rf:*)"],
 35      "ask": ["Write(/etc/*)"],
 36      "defaultMode": "default" | "plan" | "acceptEdits" | "dontAsk",
 37      "additionalDirectories": ["/extra/dir"]
 38    }
 39  }
 40  \`\`\`
 41  
 42  **Permission Rule Syntax:**
 43  - Exact match: \`"Bash(npm run test)"\`
 44  - Prefix wildcard: \`"Bash(git:*)"\` - matches \`git status\`, \`git commit\`, etc.
 45  - Tool only: \`"Read"\` - allows all Read operations
 46  
 47  ### Environment Variables
 48  \`\`\`json
 49  {
 50    "env": {
 51      "DEBUG": "true",
 52      "MY_API_KEY": "value"
 53    }
 54  }
 55  \`\`\`
 56  
 57  ### Model & Agent
 58  \`\`\`json
 59  {
 60    "model": "sonnet",  // or "opus", "haiku", full model ID
 61    "agent": "agent-name",
 62    "alwaysThinkingEnabled": true
 63  }
 64  \`\`\`
 65  
 66  ### Attribution (Commits & PRs)
 67  \`\`\`json
 68  {
 69    "attribution": {
 70      "commit": "Custom commit trailer text",
 71      "pr": "Custom PR description text"
 72    }
 73  }
 74  \`\`\`
 75  Set \`commit\` or \`pr\` to empty string \`""\` to hide that attribution.
 76  
 77  ### MCP Server Management
 78  \`\`\`json
 79  {
 80    "enableAllProjectMcpServers": true,
 81    "enabledMcpjsonServers": ["server1", "server2"],
 82    "disabledMcpjsonServers": ["blocked-server"]
 83  }
 84  \`\`\`
 85  
 86  ### Plugins
 87  \`\`\`json
 88  {
 89    "enabledPlugins": {
 90      "formatter@anthropic-tools": true
 91    }
 92  }
 93  \`\`\`
 94  Plugin syntax: \`plugin-name@source\` where source is \`claude-code-marketplace\`, \`claude-plugins-official\`, or \`builtin\`.
 95  
 96  ### Other Settings
 97  - \`language\`: Preferred response language (e.g., "japanese")
 98  - \`cleanupPeriodDays\`: Days to keep transcripts (default: 30; 0 disables persistence entirely)
 99  - \`respectGitignore\`: Whether to respect .gitignore (default: true)
100  - \`spinnerTipsEnabled\`: Show tips in spinner
101  - \`spinnerVerbs\`: Customize spinner verbs (\`{ "mode": "append" | "replace", "verbs": [...] }\`)
102  - \`spinnerTipsOverride\`: Override spinner tips (\`{ "excludeDefault": true, "tips": ["Custom tip"] }\`)
103  - \`syntaxHighlightingDisabled\`: Disable diff highlighting
104  `
105  
106  // Note: We keep hand-written examples for common patterns since they're more
107  // actionable than auto-generated schema docs. The generated schema list
108  // provides completeness while examples provide clarity.
109  
110  const HOOKS_DOCS = `## Hooks Configuration
111  
112  Hooks run commands at specific points in Claude Code's lifecycle.
113  
114  ### Hook Structure
115  \`\`\`json
116  {
117    "hooks": {
118      "EVENT_NAME": [
119        {
120          "matcher": "ToolName|OtherTool",
121          "hooks": [
122            {
123              "type": "command",
124              "command": "your-command-here",
125              "timeout": 60,
126              "statusMessage": "Running..."
127            }
128          ]
129        }
130      ]
131    }
132  }
133  \`\`\`
134  
135  ### Hook Events
136  
137  | Event | Matcher | Purpose |
138  |-------|---------|---------|
139  | PermissionRequest | Tool name | Run before permission prompt |
140  | PreToolUse | Tool name | Run before tool, can block |
141  | PostToolUse | Tool name | Run after successful tool |
142  | PostToolUseFailure | Tool name | Run after tool fails |
143  | Notification | Notification type | Run on notifications |
144  | Stop | - | Run when Claude stops (including clear, resume, compact) |
145  | PreCompact | "manual"/"auto" | Before compaction |
146  | PostCompact | "manual"/"auto" | After compaction (receives summary) |
147  | UserPromptSubmit | - | When user submits |
148  | SessionStart | - | When session starts |
149  
150  **Common tool matchers:** \`Bash\`, \`Write\`, \`Edit\`, \`Read\`, \`Glob\`, \`Grep\`
151  
152  ### Hook Types
153  
154  **1. Command Hook** - Runs a shell command:
155  \`\`\`json
156  { "type": "command", "command": "prettier --write $FILE", "timeout": 30 }
157  \`\`\`
158  
159  **2. Prompt Hook** - Evaluates a condition with LLM:
160  \`\`\`json
161  { "type": "prompt", "prompt": "Is this safe? $ARGUMENTS" }
162  \`\`\`
163  Only available for tool events: PreToolUse, PostToolUse, PermissionRequest.
164  
165  **3. Agent Hook** - Runs an agent with tools:
166  \`\`\`json
167  { "type": "agent", "prompt": "Verify tests pass: $ARGUMENTS" }
168  \`\`\`
169  Only available for tool events: PreToolUse, PostToolUse, PermissionRequest.
170  
171  ### Hook Input (stdin JSON)
172  \`\`\`json
173  {
174    "session_id": "abc123",
175    "tool_name": "Write",
176    "tool_input": { "file_path": "/path/to/file.txt", "content": "..." },
177    "tool_response": { "success": true }  // PostToolUse only
178  }
179  \`\`\`
180  
181  ### Hook JSON Output
182  
183  Hooks can return JSON to control behavior:
184  
185  \`\`\`json
186  {
187    "systemMessage": "Warning shown to user in UI",
188    "continue": false,
189    "stopReason": "Message shown when blocking",
190    "suppressOutput": false,
191    "decision": "block",
192    "reason": "Explanation for decision",
193    "hookSpecificOutput": {
194      "hookEventName": "PostToolUse",
195      "additionalContext": "Context injected back to model"
196    }
197  }
198  \`\`\`
199  
200  **Fields:**
201  - \`systemMessage\` - Display a message to the user (all hooks)
202  - \`continue\` - Set to \`false\` to block/stop (default: true)
203  - \`stopReason\` - Message shown when \`continue\` is false
204  - \`suppressOutput\` - Hide stdout from transcript (default: false)
205  - \`decision\` - "block" for PostToolUse/Stop/UserPromptSubmit hooks (deprecated for PreToolUse, use hookSpecificOutput.permissionDecision instead)
206  - \`reason\` - Explanation for decision
207  - \`hookSpecificOutput\` - Event-specific output (must include \`hookEventName\`):
208    - \`additionalContext\` - Text injected into model context
209    - \`permissionDecision\` - "allow", "deny", or "ask" (PreToolUse only)
210    - \`permissionDecisionReason\` - Reason for the permission decision (PreToolUse only)
211    - \`updatedInput\` - Modified tool input (PreToolUse only)
212  
213  ### Common Patterns
214  
215  **Auto-format after writes:**
216  \`\`\`json
217  {
218    "hooks": {
219      "PostToolUse": [{
220        "matcher": "Write|Edit",
221        "hooks": [{
222          "type": "command",
223          "command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; prettier --write \\"$f\\"; } 2>/dev/null || true"
224        }]
225      }]
226    }
227  }
228  \`\`\`
229  
230  **Log all bash commands:**
231  \`\`\`json
232  {
233    "hooks": {
234      "PreToolUse": [{
235        "matcher": "Bash",
236        "hooks": [{
237          "type": "command",
238          "command": "jq -r '.tool_input.command' >> ~/.claude/bash-log.txt"
239        }]
240      }]
241    }
242  }
243  \`\`\`
244  
245  **Stop hook that displays message to user:**
246  
247  Command must output JSON with \`systemMessage\` field:
248  \`\`\`bash
249  # Example command that outputs: {"systemMessage": "Session complete!"}
250  echo '{"systemMessage": "Session complete!"}'
251  \`\`\`
252  
253  **Run tests after code changes:**
254  \`\`\`json
255  {
256    "hooks": {
257      "PostToolUse": [{
258        "matcher": "Write|Edit",
259        "hooks": [{
260          "type": "command",
261          "command": "jq -r '.tool_input.file_path // .tool_response.filePath' | grep -E '\\\\.(ts|js)$' && npm test || true"
262        }]
263      }]
264    }
265  }
266  \`\`\`
267  `
268  
269  const HOOK_VERIFICATION_FLOW = `## Constructing a Hook (with verification)
270  
271  Given an event, matcher, target file, and desired behavior, follow this flow. Each step catches a different failure class — a hook that silently does nothing is worse than no hook.
272  
273  1. **Dedup check.** Read the target file. If a hook already exists on the same event+matcher, show the existing command and ask: keep it, replace it, or add alongside.
274  
275  2. **Construct the command for THIS project — don't assume.** The hook receives JSON on stdin. Build a command that:
276     - Extracts any needed payload safely — use \`jq -r\` into a quoted variable or \`{ read -r f; ... "$f"; }\`, NOT unquoted \`| xargs\` (splits on spaces)
277     - Invokes the underlying tool the way this project runs it (npx/bunx/yarn/pnpm? Makefile target? globally-installed?)
278     - Skips inputs the tool doesn't handle (formatters often have \`--ignore-unknown\`; if not, guard by extension)
279     - Stays RAW for now — no \`|| true\`, no stderr suppression. You'll wrap it after the pipe-test passes.
280  
281  3. **Pipe-test the raw command.** Synthesize the stdin payload the hook will receive and pipe it directly:
282     - \`Pre|PostToolUse\` on \`Write|Edit\`: \`echo '{"tool_name":"Edit","tool_input":{"file_path":"<a real file from this repo>"}}' | <cmd>\`
283     - \`Pre|PostToolUse\` on \`Bash\`: \`echo '{"tool_name":"Bash","tool_input":{"command":"ls"}}' | <cmd>\`
284     - \`Stop\`/\`UserPromptSubmit\`/\`SessionStart\`: most commands don't read stdin, so \`echo '{}' | <cmd>\` suffices
285  
286     Check exit code AND side effect (file actually formatted, test actually ran). If it fails you get a real error — fix (wrong package manager? tool not installed? jq path wrong?) and retest. Once it works, wrap with \`2>/dev/null || true\` (unless the user wants a blocking check).
287  
288  4. **Write the JSON.** Merge into the target file (schema shape in the "Hook Structure" section above). If this creates \`.claude/settings.local.json\` for the first time, add it to .gitignore — the Write tool doesn't auto-gitignore it.
289  
290  5. **Validate syntax + schema in one shot:**
291  
292     \`jq -e '.hooks.<event>[] | select(.matcher == "<matcher>") | .hooks[] | select(.type == "command") | .command' <target-file>\`
293  
294     Exit 0 + prints your command = correct. Exit 4 = matcher doesn't match. Exit 5 = malformed JSON or wrong nesting. A broken settings.json silently disables ALL settings from that file — fix any pre-existing malformation too.
295  
296  6. **Prove the hook fires** — only for \`Pre|PostToolUse\` on a matcher you can trigger in-turn (\`Write|Edit\` via Edit, \`Bash\` via Bash). \`Stop\`/\`UserPromptSubmit\`/\`SessionStart\` fire outside this turn — skip to step 7.
297  
298     For a **formatter** on \`PostToolUse\`/\`Write|Edit\`: introduce a detectable violation via Edit (two consecutive blank lines, bad indentation, missing semicolon — something this formatter corrects; NOT trailing whitespace, Edit strips that before writing), re-read, confirm the hook **fixed** it. For **anything else**: temporarily prefix the command in settings.json with \`echo "$(date) hook fired" >> /tmp/claude-hook-check.txt; \`, trigger the matching tool (Edit for \`Write|Edit\`, a harmless \`true\` for \`Bash\`), read the sentinel file.
299  
300     **Always clean up** — revert the violation, strip the sentinel prefix — whether the proof passed or failed.
301  
302     **If proof fails but pipe-test passed and \`jq -e\` passed**: the settings watcher isn't watching \`.claude/\` — it only watches directories that had a settings file when this session started. The hook is written correctly. Tell the user to open \`/hooks\` once (reloads config) or restart — you can't do this yourself; \`/hooks\` is a user UI menu and opening it ends this turn.
303  
304  7. **Handoff.** Tell the user the hook is live (or needs \`/hooks\`/restart per the watcher caveat). Point them at \`/hooks\` to review, edit, or disable it later. The UI only shows "Ran N hooks" if a hook errors or is slow — silent success is invisible by design.
305  `
306  
307  const UPDATE_CONFIG_PROMPT = `# Update Config Skill
308  
309  Modify Claude Code configuration by updating settings.json files.
310  
311  ## When Hooks Are Required (Not Memory)
312  
313  If the user wants something to happen automatically in response to an EVENT, they need a **hook** configured in settings.json. Memory/preferences cannot trigger automated actions.
314  
315  **These require hooks:**
316  - "Before compacting, ask me what to preserve" → PreCompact hook
317  - "After writing files, run prettier" → PostToolUse hook with Write|Edit matcher
318  - "When I run bash commands, log them" → PreToolUse hook with Bash matcher
319  - "Always run tests after code changes" → PostToolUse hook
320  
321  **Hook events:** PreToolUse, PostToolUse, PreCompact, PostCompact, Stop, Notification, SessionStart
322  
323  ## CRITICAL: Read Before Write
324  
325  **Always read the existing settings file before making changes.** Merge new settings with existing ones - never replace the entire file.
326  
327  ## CRITICAL: Use AskUserQuestion for Ambiguity
328  
329  When the user's request is ambiguous, use AskUserQuestion to clarify:
330  - Which settings file to modify (user/project/local)
331  - Whether to add to existing arrays or replace them
332  - Specific values when multiple options exist
333  
334  ## Decision: Config Tool vs Direct Edit
335  
336  **Use the Config tool** for these simple settings:
337  - \`theme\`, \`editorMode\`, \`verbose\`, \`model\`
338  - \`language\`, \`alwaysThinkingEnabled\`
339  - \`permissions.defaultMode\`
340  
341  **Edit settings.json directly** for:
342  - Hooks (PreToolUse, PostToolUse, etc.)
343  - Complex permission rules (allow/deny arrays)
344  - Environment variables
345  - MCP server configuration
346  - Plugin configuration
347  
348  ## Workflow
349  
350  1. **Clarify intent** - Ask if the request is ambiguous
351  2. **Read existing file** - Use Read tool on the target settings file
352  3. **Merge carefully** - Preserve existing settings, especially arrays
353  4. **Edit file** - Use Edit tool (if file doesn't exist, ask user to create it first)
354  5. **Confirm** - Tell user what was changed
355  
356  ## Merging Arrays (Important!)
357  
358  When adding to permission arrays or hook arrays, **merge with existing**, don't replace:
359  
360  **WRONG** (replaces existing permissions):
361  \`\`\`json
362  { "permissions": { "allow": ["Bash(npm:*)"] } }
363  \`\`\`
364  
365  **RIGHT** (preserves existing + adds new):
366  \`\`\`json
367  {
368    "permissions": {
369      "allow": [
370        "Bash(git:*)",      // existing
371        "Edit(.claude)",    // existing
372        "Bash(npm:*)"       // new
373      ]
374    }
375  }
376  \`\`\`
377  
378  ${SETTINGS_EXAMPLES_DOCS}
379  
380  ${HOOKS_DOCS}
381  
382  ${HOOK_VERIFICATION_FLOW}
383  
384  ## Example Workflows
385  
386  ### Adding a Hook
387  
388  User: "Format my code after Claude writes it"
389  
390  1. **Clarify**: Which formatter? (prettier, gofmt, etc.)
391  2. **Read**: \`.claude/settings.json\` (or create if missing)
392  3. **Merge**: Add to existing hooks, don't replace
393  4. **Result**:
394  \`\`\`json
395  {
396    "hooks": {
397      "PostToolUse": [{
398        "matcher": "Write|Edit",
399        "hooks": [{
400          "type": "command",
401          "command": "jq -r '.tool_response.filePath // .tool_input.file_path' | { read -r f; prettier --write \\"$f\\"; } 2>/dev/null || true"
402        }]
403      }]
404    }
405  }
406  \`\`\`
407  
408  ### Adding Permissions
409  
410  User: "Allow npm commands without prompting"
411  
412  1. **Read**: Existing permissions
413  2. **Merge**: Add \`Bash(npm:*)\` to allow array
414  3. **Result**: Combined with existing allows
415  
416  ### Environment Variables
417  
418  User: "Set DEBUG=true"
419  
420  1. **Decide**: User settings (global) or project settings?
421  2. **Read**: Target file
422  3. **Merge**: Add to env object
423  \`\`\`json
424  { "env": { "DEBUG": "true" } }
425  \`\`\`
426  
427  ## Common Mistakes to Avoid
428  
429  1. **Replacing instead of merging** - Always preserve existing settings
430  2. **Wrong file** - Ask user if scope is unclear
431  3. **Invalid JSON** - Validate syntax after changes
432  4. **Forgetting to read first** - Always read before write
433  
434  ## Troubleshooting Hooks
435  
436  If a hook isn't running:
437  1. **Check the settings file** - Read ~/.claude/settings.json or .claude/settings.json
438  2. **Verify JSON syntax** - Invalid JSON silently fails
439  3. **Check the matcher** - Does it match the tool name? (e.g., "Bash", "Write", "Edit")
440  4. **Check hook type** - Is it "command", "prompt", or "agent"?
441  5. **Test the command** - Run the hook command manually to see if it works
442  6. **Use --debug** - Run \`claude --debug\` to see hook execution logs
443  `
444  
445  export function registerUpdateConfigSkill(): void {
446    registerBundledSkill({
447      name: 'update-config',
448      description:
449        'Use this skill to configure the Claude Code harness via settings.json. Automated behaviors ("from now on when X", "each time X", "whenever X", "before/after X") require hooks configured in settings.json - the harness executes these, not Claude, so memory/preferences cannot fulfill them. Also use for: permissions ("allow X", "add permission", "move permission to"), env vars ("set X=Y"), hook troubleshooting, or any changes to settings.json/settings.local.json files. Examples: "allow npm commands", "add bq permission to global settings", "move permission to user settings", "set DEBUG=true", "when claude stops show X". For simple settings like theme/model, use Config tool.',
450      allowedTools: ['Read'],
451      userInvocable: true,
452      async getPromptForCommand(args) {
453        if (args.startsWith('[hooks-only]')) {
454          const req = args.slice('[hooks-only]'.length).trim()
455          let prompt = HOOKS_DOCS + '\n\n' + HOOK_VERIFICATION_FLOW
456          if (req) {
457            prompt += `\n\n## Task\n\n${req}`
458          }
459          return [{ type: 'text', text: prompt }]
460        }
461  
462        // Generate schema dynamically to stay in sync with types
463        const jsonSchema = generateSettingsSchema()
464  
465        let prompt = UPDATE_CONFIG_PROMPT
466        prompt += `\n\n## Full Settings JSON Schema\n\n\`\`\`json\n${jsonSchema}\n\`\`\``
467  
468        if (args) {
469          prompt += `\n\n## User Request\n\n${args}`
470        }
471  
472        return [{ type: 'text', text: prompt }]
473      },
474    })
475  }