-
Notifications
You must be signed in to change notification settings - Fork 0
Hook & Slash Command Settings
Two settings-backed lists look similar in the UI but come from very different places, and the two core modules behind them reflect that split. Agent hooks are read off the filesystem from CLI configuration files, so their module is defensive about I/O, parse failures and secrets. Slash commands are defined in-process, so their module is mostly a table plus a consistency gate against the chat parser. Neither module renders anything, speaks IPC, or imports the other; both are data producers that hand back SettingsHook[] or SettingsCommand[] to whatever layer serves settings (Electron main, or the server-edition RPC side).
This page follows the mechanism first, then what each file owns, the invariants to preserve, and the seams where changes are expected.
inventoryHooks(files) in src/core/settings-hooks.ts is the entire read path. It never discovers files itself: the caller supplies a HookFile[], each entry pairing a filesystem path with the agent id that file belongs to and an optional source ('managed' | 'legacy'). Keeping the file list caller-supplied is the important separation — which CLI keeps hooks in which file is knowledge owned by the installers that write those files (see the Hook Server & CLI Hook Installers page), while this module owns only the text-to-record conversion.
flowchart TD
A["Caller builds HookFile[]<br/>path + agent + optional source"] --> B["inventoryHooks(files)"]
B --> C["readFileSync(path, 'utf8') per entry"]
C -->|throws| X["file contributes nothing"]
C --> D["parseHookText(raw, agent, source)"]
D -.-> S["classification: explicit source wins,<br/>else raw contains __termsprawl → 'managed'"]
D --> E{"JSON.parse succeeds?"}
E -->|yes| F["top-level event keys, entries[].hooks[];<br/>command = hook.url ?? hook.command"]
F --> G{"at least one hook?"}
G -->|yes| H["parsed hooks"]
G -->|no| I["line fallback:<br/>event keyword line + command=/url= quoted value"]
E -->|no| I
I --> H
H --> J["flatten across files, sort by id"]
J --> K["SettingsHook[] for the settings list"]
Key nodes in that flow:
-
Read error (
throws) —inventoryHookswrapsparseHookText(readFileSync(path, 'utf8'), …)per file intry/catchand contributes[]for that file. One unreadable file never removes hooks from other files, and a malformed config never throws into the caller. The trade-off is silence; see Boundary conditions. -
Classification — the caller-provided
sourcewins. Otherwise the raw text is tested with/__termsprawl(?:Managed)?/: a match marks the file'managed', meaning it was written by this app's hook installers; anything else is'legacy'. This is a coarse, whole-file, substring-level check. -
JSON branch — the raw text is parsed as a JSON object whose top-level keys are hook event names. For each key whose value is an array, each entry's
hooks[]array is walked, and a hook is taken fromhook.url ?? hook.command(URL wins when both exist), guarded by a type check on the extracted value. Non-array values and entries without ahooksarray are ignored. - Short-circuit — if the JSON branch produced at least one hook, it returns immediately and the line fallback never runs. Only a parse failure or a successful parse that yields zero hooks falls through.
-
Line fallback — for legacy, line-oriented configs. Each line is scanned for an event keyword; a match sets the current event and it stays current for following lines. A line matching
(?:command|url)\s*=\s*["']([^"']+)contributes a hook for the current event. Values must be quoted; unquoted assignments are skipped. -
Sorting —
inventoryHooksflattens all per-file results and sorts byid(a.id.localeCompare(b.id)), so the settings list has stable ordering regardless of file order or read order.
The event keywords the fallback recognizes are baked into one alternation: PreToolUse, PostToolUse, Notification, Stop, UserPromptSubmit, PermissionRequest, SubagentStop, SessionStart, SessionEnd, SubagentStart, PreCompact, PostCompact. An event name outside that list is invisible to the fallback parser; the JSON branch does not depend on this list because it uses the object keys.
Hook commands and URLs are prime carriers for API keys and tokens, and these records end up in a settings list, so the module exports redactHookSecret(s). It applies two patterns:
- Query-string parameters named
key,token,secret,api_keyorpassword(case-insensitive): the value after=is replaced with[redacted], the parameter name is preserved. - Standalone tokens with an
sk_orghp_prefix (case-sensitive, word-bounded): replaced with[redacted].
Both patterns are name/prefix-specific. A token shape they do not cover — a different prefix, a bare secret passed as a positional argument — passes through unchanged, so "redacted" must not be read as "contains no secrets". The excerpt shows the function being exported rather than its call sites, so any new display path for hook strings should call it deliberately.
src/core/settings-commands.ts is much smaller because nothing is read from disk. BUILTIN_COMMANDS is a module-level SettingsCommand[] describing the commands the app itself implements. Everything visible in the excerpt has source: "built-in" and available: true:
| Command | Description |
|---|---|
/clear |
Clear the conversation |
/model |
Change the chat model |
/system |
Set the system prompt |
/cost |
(description truncated in the excerpt) |
The array continues past what the excerpt shows; the table is only what is visible.
Two functions wrap that constant:
-
discoverCommands()returnsBUILTIN_COMMANDS.map(x => ({...x}))— a fresh array of copies. Callers can sort, filter or annotate the result without mutating the module constant, so command discovery is per-call state, not shared state. At this level only built-ins are discoverable;SettingsCommand.sourceand.availableleave room for other origins, but nothing in this module produces them today. -
commandsHaveParserParity()checks that every builtin name resolves to something truthy atdetectSlashCommand(name)?.command, wheredetectSlashCommandcomes fromsrc/core/chat/conversation.ts. This is the cross-module contract: the list that advertises commands and the parser that recognizes them cannot drift. Adding a row toBUILTIN_COMMANDSwithout teaching the chat parser about it makes this check fail.
The module also re-exports detectSlashCommand, so a settings or input consumer can classify typed text through the same parser that dispatch uses instead of growing a second matcher. No call site for commandsHaveParserParity() is visible in the file; it reads as a regression guard for tests or a startup assertion, and should stay in that role if the command surface grows.
The call chain that matters for changes is thin, and its ends are outside these two files:
- The settings data path (main process for the Electron build, RPC handlers for the server edition) asks for the hooks list and the commands list.
- Hooks path: the caller assembles
HookFile[]from the CLI config locations it knows about, callsinventoryHooks, and forwards theSettingsHook[]— running string fields throughredactHookSecretbefore anything is displayed. - Commands path: the caller calls
discoverCommands()for the list, and uses the re-exporteddetectSlashCommandwherever typed input must be classified. -
commandsHaveParserParity()is orthogonal to rendering and belongs wherever a regression should fail loudly.
Because hook inventory re-reads the filesystem on every call and caches nothing, a settings reload naturally reflects edits to a CLI config file. Because command discovery copies a constant, its content changes only when code changes.
The full public surface, all stateless:
-
HookFile— input descriptor:path(filesystem path to a candidate hook config),agent(attribution label threaded through parsing), optionalsourceto override classification. -
redactHookSecret(s)— the secret-sanitizing helper for this area; coverage described above. -
parseHookText(raw, agent, source?)— pure text-to-records conversion using the JSON-then-lines strategy. It does not throw: JSON errors are caught internally, and the line fallback always runs when the JSON branch yields nothing. -
inventoryHooks(files)— the I/O layer: reads each file, isolates failures per file, flattens and sorts byid.
There is no cache and no module-level mutable data, which is why the same function can back both a first render and a later refresh, from Electron main or from server-edition handlers. It is not renderer-safe: it imports node:fs directly, so renderer code must reach hooks through the IPC/RPC bridge rather than importing this module.
-
BUILTIN_COMMANDS— the single source of truth for what the settings list shows, withname,description,sourceandavailableon every entry. -
discoverCommands()— clone-based accessor; the seam where additional command sources would be merged, sinceSettingsCommandalready models an origin field that this module only ever sets to'built-in'. -
commandsHaveParserParity()— the contract check against the chat parser. - The
detectSlashCommandre-export — a forwarding alias forsrc/core/chat/conversation.ts, so consumers need one import for both listing and recognition.
-
src/shared/types.ts(the module's../shared/typesimport) suppliesSettingsHookandSettingsCommand; both core modules are typed against it, and field changes ripple into the settings UI. -
src/core/chat/conversation.ts(the./chat/conversationimport) suppliesdetectSlashCommand, the parser authority that the parity check validates against. - The hook writers (Hook Server & CLI Hook Installers) own the paths and file contents that
inventoryHookslater reads; the two sides share a file-format contract but no code.
-
No cache, no shared mutable state.
inventoryHooksreflects disk at call time;discoverCommandsreturns copies;BUILTIN_COMMANDSis effectively read-only. - Per-file failure isolation. A missing, unreadable or malformed hook file contributes an empty list, never an exception.
-
Stable ordering. Hooks are sorted by
id; treatSettingsHook.idas the identity used for list keys and dedupe. - JSON-first parsing. A file that parses as JSON and yields at least one hook is never consulted as line-oriented text.
-
Parser parity. Every builtin command name must be recognized by
detectSlashCommand; that is the invariantcommandsHaveParserParityencodes.
-
Silent hook failures.
inventoryHooksswallows read and parse errors. A corrupted managed hook file simply makes the settings list shorter, with no diagnostic channel. If "file X could not be read" must be surfaced, thetry/catchininventoryHooksis the place to change the return shape. -
Whole-file classification. The
__termsprawlmarker check runs over the entire raw text, not per command, so a file mixing app-managed and hand-written hooks is labeled by whichever marker appears anywhere in it. -
URL priority.
hook.url ?? hook.commandmeans a hook object carrying both fields silently drops the command. -
JSON shape strictness. Only top-level array values and each entry's
hooksarray are walked. Other plausible shapes fall through to the line parser, which may still recover event/command text but loses structure. -
Quoting in the fallback.
command=/url=values must be quoted; a command line with no preceding event keyword is dropped because the current event is still undefined. The event is sticky across lines, which is correct for grouped formats and wrong for free-form files that interleave. -
Redaction coverage. Only the named query parameters and
sk_/ghp_token shapes are replaced; the token pattern is case-sensitive and prefix-specific. -
Parity covers builtins only. If
discoverCommands()is extended beyondBUILTIN_COMMANDS,commandsHaveParserParity()will not check the new entries until it is rewritten to iterate the discovered set. -
Renderer import hazard.
settings-hooks.tspulls innode:fs; importing it from renderer code breaks the bundle boundary.
-
New agent CLI: add its config file as a
HookFilewithpathandagent; passsource: 'managed'when provenance is known, skipping the marker sniff. -
New hook event in legacy files: extend the event alternation inside
parseHookText's fallback; JSON-written configs pick new keys up automatically. -
New builtin command: add an entry to
BUILTIN_COMMANDSand make suredetectSlashCommandrecognizes the name, then let the parity check confirm it. -
New command source (project- or user-scoped): extend
discoverCommands()to merge additional entries;SettingsCommand.source/availablealready model origin and enablement. -
Surfacing parse errors: change
inventoryHooksto return per-file diagnostics (or add a sibling function) instead of the current empty-list fallback. -
New secret shapes: extend the regexes in
redactHookSecretand apply redaction consistently at every display boundary, since it is not automatic.
- The excerpt truncates the long lines inside
parseHookText(lines 8 and 10) and theBUILTIN_COMMANDSliteral beyond/cost. The exact field mapping intoSettingsHookand the full command inventory cannot be verified from this page; read the files directly. -
SettingsHookandSettingsCommandare imported fromsrc/shared/types.ts, which is not included in the excerpt. -
detectSlashCommand's matching rules live insrc/core/chat/conversation.ts; only the fact that a recognized name yields a truthy.commandis evidenced here. - The consumer side — IPC/RPC channel names, list components, and whether either inventory is also persisted in the app settings store — is outside these two files; consult the Settings Panel & Capability Pages and the Preload Bridge & IPC Contract pages.
Sources: src/core/settings-hooks.ts, src/core/settings-commands.ts
Generated from termsprawl at 0d4393be54c6200beedd91bb636e5296c30472c5.
App Shell & Platform Foundations
- Electron Main Process & Window Lifecycle
- Preload Bridge & IPC Contract
- Shared Domain Types and File/URL Helpers
- Renderer Bootstrap & App Composition
- Build Targets & TypeScript Configuration
Canvas, Nodes & Renderer State
- Infinite Canvas Surface & Viewport Interaction
- Workspace, Project & Tab State
- Node Links, Edges & Link Inspector
- Sticky, Group, Editor & Diff Nodes
- Keyboard Canvas Navigation & Cross-Panel Requests
- Theme, Accent & Visual Language
- Boot Overlay, Onboarding & Shared UI Kit
Terminals & Session Continuity
- PTY Lifecycle & Terminal Sessions
- tmux Session Naming & Reattach
- Scrollback Snapshots & Cold Replay
- Terminal Node Rendering (xterm.js)
- SSH Remote Projects, Terminals & Files
Persistence, Projects & Files
- Workspace Store & Project File Layout
- Project Scope, Deletion & Worktree Registry
- Workspace Bundle Export/Import
- File Service & File Tree UI
Agent Runtime & Tooling
- Agent Status Model & Hook Normalization
- Hook Server & CLI Hook Installers
- Agent Launch, CLI Probing & Managed Accounts
- Agent Tool Protocol & In-Process Server
- Agent Tool Client, CLI & MCP Entry
- Transcripts, Context Discovery & Context CLI
- Agent Canvas State & Status Badges
Chat Nodes & Model Providers
- Chat Runtime, Conversation & Cost
- Model Provider Adapters & Streaming
- Chat Tool Calling & Project Tools
- Chat Node UI
Git & Source Control
Embedded Browser Nodes
- Browser Manager & Guest Runtime
- CDP Facade & Browser Agent Server
- Browser Navigation Policy & Node UI
Server Edition
- Server Bootstrap & HTTP/WebSocket Entry
- RPC Dispatch, Handlers & Service Bridges
- Renderer Shim & Server Boundary
- Server Auth & Security Boundary
Relay & Remote Access
- Relay Hub & WebSocket Frame Routing
- Relay End-to-End Cryptography
- Relay Auth, Invites, Store & Admin API
- Relay Client, Pairing & Terminal Tunneling
- Relay Trust UI
Integrations & Secondary Surfaces
- Telegram Bot, Commands & Pairing
- A2A Peers: Protocol, Client & Server
- Node Link Engine, Registry & Scheduler
- Cloud Spaces, Snapshots & Sync
Settings, Updates & Maintenance