mind-cli 0.22.0

A manager for agent tooling (skills, agents, rules, tools) that melds arbitrary git repos and links items into your agent directories.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
# Install hooks

Status: done. Some agent libraries ship tooling (binaries, scripts) that their
skills or agents depend on, which must be built or installed before the items
work. An install hook is a command a source declares (or a user supplies) that
`mind` runs to perform that setup. Because the command is arbitrary code from the
source, `mind` shows it and prompts before running it.

## Overview

An install hook is a shell command associated with a source. The maintainer
declares it in `mind.toml`, or a user supplies one on the command line (for a repo
that ships no `mind.toml`, or to override the declared one, in which case the
override is shown loudly). The hook runs in the source's clone during `meld`, and
re-runs during `upgrade` when the source updates.

The hook is arbitrary code execution: it can run any command with the user's
privileges. So `mind` never runs a hook without first showing the user what will
run and where it came from (source identity, the pin and commit, the clone path,
and the exact command), with an explicit warning, framed by a header so the
disclosure is visibly distinct from the surrounding output. The interactive
prompt offers run (the default, a bare Enter), skip the hook but still install the
source and its items, or abort and install nothing. In a non-TTY context the hook
is skipped instead (HOOK-22); `--dangerously-skip-install-hook-check` runs it
unattended for users who have already vetted the source. The prompt is the trust
boundary; the shown pin and commit let the user inspect the repo at that exact
point before approving.

The rest of this document states the rules normatively. Source identity is
`host/owner/repo` (see storage.md); a source's pin is its `Pin` (see cli.md,
CLI-17).

## Declaring a hook

- `HOOK-1` `[source].install` in a repo's `mind.toml` is a shell command string
  that declares the source's install hook (run to build or install the tooling
  the source's items rely on). Deprecated in favor of a `[[hooks]]` install entry
  (HOOK-90); still parsed and folded into the hook set (HOOK-50) for back-compat.
- `HOOK-2` `mind meld <repo> --install-hook <cmd>` supplies an install hook, both
  for a repo with no `mind.toml` and to override one a source declares. When it
  overrides a declared `[source].install`, the override is loud and obvious: the
  safety prompt (HOOK-20) shows the source's declared command and the overriding
  command, and states that the user-supplied command is what will run, so the user
  cannot miss that they replaced the maintainer's hook.
- `HOOK-3` With no `[source].install` and no `--install-hook`, `meld` runs no hook
  (behavior is unchanged from a source without one). An empty or whitespace-only value
  for either field is treated the same as absent: no hook runs and nothing is recorded.

## When a hook runs

- `HOOK-10` An install hook runs during `meld`, once, in the source's clone
  directory, after the working tree is checked out at the resolved pin. It is a
  source-level step; `learn` does not run hooks.
- `HOOK-11` `upgrade` re-runs a source's hook (subject to the same prompt) when the
  source has advanced to a new commit and a hook is in effect, so the tooling
  tracks the source. `sync` alone (which only fetches and records the new commit)
  does not run the hook.

## The safety prompt

- `HOOK-20` Before running any hook, `mind` prints a disclosure framed by a
  `====== hook: <name> ======` header (so it is visibly distinct from the
  surrounding `melded <source>` output): the source identity, the resolved pin
  (the branch, tag, or ref) and the commit, the clone path, and the exact command
  that will run, with a clear warning that this executes arbitrary code from the
  source. It then offers `[Y/n/a]`: run the hook (`y`/`Y`/Enter), skip it but
  continue installing the source and its items without building the tooling
  (`n`/`N`), or abort and install nothing (`a`/`A`). The default (a bare Enter) is
  run; an unrecognized reply skips, so an unclear answer never runs the hook. When
  a `--install-hook` overrides the source's declared `[source].install` (HOOK-2),
  the prompt also shows the declared command and states plainly that the
  user-supplied command is replacing it.
- `HOOK-21` Skipping the hook (`n`) still installs the source and its items: only
  the declared tooling is not built. `mind` says so and notes the items may not
  work until the hook is run. Aborting (`a`) installs nothing: the source is not
  registered, as for a declined meld.
- `HOOK-22` When stdin is not a TTY (a script, CI, or managed-policy auto-meld,
  POL-32), `mind` never runs a hook silently and never aborts: regardless of the
  interactive default (HOOK-20), it takes the skip path (the source and its items
  install, the tooling is not built) and reports it, unless
  `--dangerously-skip-install-hook-check` is given.
- `HOOK-23` `--dangerously-skip-install-hook-check` runs the hook without
  prompting, and is what enables hooks in non-interactive use. The flag name is
  deliberately explicit about the risk.
- `HOOK-24` The consent disclosure (HOOK-20) makes both ways to inspect
  the code one action away. It labels the on-disk clone path as its own line (the
  path HOOK-20 already shows), so the user can open the working tree that will run
  locally, and it adds a version-control browse URL pinned to the disclosed commit,
  so the user can read the exact code in the forge without cloning. The URL is
  derived with the same host rules as the compare URL (CLI-176, CLI-188): an https
  GitHub-shaped host yields `https://<host>/<owner>/<repo>/tree/<commit>`; a host
  whose name contains "gitlab" or "bitbucket", an SSH remote, and a local or
  `file://` source yield no URL (only the clone path is shown, since no correct web
  link exists for them). The browse URL is source-derived text and is sanitized on
  the consent surface exactly as the other disclosure fields are (HOOK-91).
- `HOOK-91` All source-derived text included in the hook consent disclosure
  (identity, pin description, commit, clone path, hook command, declared
  command) is sanitized -- ANSI escape sequences, C0/DEL/C1 control characters,
  and Unicode bidi-override/separator code points stripped -- before the
  disclosure string is printed. This prevents a malicious source from using
  cursor/line-clear escapes to overwrite the warning line or bidi-reorder the
  displayed command to look innocuous on the exact surface where the user
  consents to run arbitrary code. Static literal text in the disclosure (the
  WARNING line, field labels) is not sanitized (it is not untrusted). The same
  sanitization rule applies to the hook label shown in multi-hook disclosures
  (HOOK-52). This is consistent with the TUI model-boundary rule (TUI-60) and
  extends it to the CLI consent prompt.

## Execution and recording

- `HOOK-30` A hook runs via the shell in the clone directory with its stdin
  closed (it cannot consume `mind`'s input). Its stdout and stderr are captured
  and mirrored to `mind`'s output under labeled separators -- the captured stdout
  under `====== (hook-stdout: <name>) ======` and the captured stderr under
  `====== (hook-stderr: <name>) ======`, each block omitted when that stream is
  empty, with a closing `====== (end hook: <name>) ======` divider when any output
  was shown -- so the hook's output is clearly demarcated from `mind`'s own and
  from whatever it prints next (e.g. the install preview). A
  non-zero exit is a `HookFailed` error and fails the `meld`: the source is not
  left registered (the clone is removed, as for any failed meld), and the error
  points to the framed output already shown rather than repeating it. Side effects
  the hook already had on the system (an installed binary, a global package) are
  outside `mind`'s state and are not rolled back.
- `HOOK-31` `mind` records the in-effect hook command and the commit it ran at on
  the source registry entry, so `upgrade` can detect a changed command or commit
  and re-prompt (HOOK-11), and `recall` / `introspect` can report that a source
  has an install hook.

## Validation (`mind review`)

- `HOOK-40` `mind review <target>` reports a source's declared `[source].install`
  hook as an advisory finding (showing the command), so a consumer can see, before
  melding, that the source will ask to run code, and a maintainer can confirm the
  hook is what they intend to publish (CLI-130).

## Multiple hooks and lifecycle events

A source may declare more than one hook, for two lifecycle events (install at
`meld`, uninstall at `unmeld`), and mark a hook optional so the user can skip
that step. `[[hooks]]` is the canonical form; the single `[source].install`
string (HOOK-1) is a deprecated shorthand for one required install hook (HOOK-90),
still parsed and folded in (HOOK-50).

- `HOOK-50` A source declares hooks via a `[[hooks]]` array-of-tables in
  `mind.toml`. Each hook runs, in declaration order, in the source's clone
  directory at its lifecycle event. The legacy `[source].install` (HOOK-1) is
  exactly equivalent to one `[[hooks]]` with that command, `optional = false`,
  `event = "install"`, folded in ahead of any declared `[[hooks]]`. An empty or
  whitespace-only `run` is treated as absent (HOOK-3) and contributes no hook.
- `HOOK-51` A `[[hooks]]` entry's fields are: `run` (the shell command,
  required), `name` (an optional label shown in the disclosure; defaults to the
  command), `optional` (bool, default `false` = required), and `event`
  (`"install"` or `"uninstall"`, default `"install"`). An unknown `event` value
  is a `mind.toml` schema error naming the bad value and the legal set.
- `HOOK-52` An optional hook (`optional = true`) is disclosed like any hook but
  prompted with a two-way `[Y/n]` choice: run it (`y`/`Y`/Enter, the default), or
  skip it (`n`/`N`). `optional` means the user may decline to run the step (skip
  it); it offers no abort because skipping is the graceful decline. A required hook
  keeps the three-way `[Y/n/a]` prompt (HOOK-20). The interactive default is run
  for both; in a non-TTY context every hook is skipped instead (HOOK-22), and
  `--dangerously-skip-install-hook-check` runs every hook (HOOK-23), optional and
  required alike. `optional` does NOT make the hook's failure tolerable (HOOK-53).
- `HOOK-53` Any hook's non-zero exit is a hard stop, whether the hook is optional
  or required: at `meld` the clone is removed and nothing is registered
  (HOOK-30); at `unmeld` the unmeld stops and the source remains. `optional`
  governs only whether the user may decline to run the hook (HOOK-52), never
  whether it may fail.
- `HOOK-60` When a hook runs (a chosen run, or an unattended run under
  `--dangerously-skip-install-hook-check`), `mind` prints a line naming the hook
  before running it, so the user sees which step is executing. Re-melding an
  already-melded source (CLI-12) re-offers its install hooks that have not run at
  the source's current commit (a hook skipped at an earlier meld, or added since),
  with the same disclosure and prompt as a fresh meld, before the install step.
  So `mind meld` in an already-melded project still prompts for a pending optional
  or required hook. `meld --force` re-offers ALL of the source's install hooks on
  a re-meld, even those already run at the current commit (alongside forcing the
  clobber overwrite, CLI-35).
- `HOOK-54` Uninstall hooks (`event = "uninstall"`) run at `unmeld`, in the
  source's clone, before the clone and registry entry are removed (so cleanup can
  use the working tree). On the default unmeld path (CLI-21), the multi-item
  removal confirmation (CLI-42) runs first; uninstall hooks only run after the
  user confirms (or `--yes` skips the confirm). A user who declines the confirm
  does not trigger any hook. On the `--unlink-only` path (CLI-22), which has no
  multi-item confirm, hooks run before the source is removed. They use
  the same prompt model as install hooks: required = run / skip / abort-the-unmeld;
  optional = run / skip; a non-TTY `unmeld` skips them and notes it; `mind unmeld
  --dangerously-skip-install-hook-check` runs them unattended. A required
  uninstall hook that fails or is aborted leaves the source melded. Item uninstall
  hooks run before source uninstall hooks, making teardown the reverse of install
  (HOOK-87).
- `HOOK-55` Install hooks are recorded as a set on the source's registry entry
  (`install_hooks`: each an effective command plus the commit it last ran at, or
  null when skipped), superseding the single `[source].install`/commit pair
  (HOOK-31), which is migrated into the set when an older `sources.json` is
  loaded. `upgrade` re-offers each install hook that is pending: a hook is pending
  when its recorded run-commit is null (never ran or was skipped), or when it
  differs from the source's current commit (the source advanced). A null run-commit
  is always treated as pending regardless of whether the source's commit is also
  null (a commitless linked source). Uninstall hooks are not recorded, since they
  only fire at `unmeld`.
- `HOOK-56` `meld --install-hook <cmd>` (HOOK-2) replaces all of a source's
  declared install hooks with one required install hook running `<cmd>`; the loud
  override disclosure (HOOK-2) shows the declared command(s) it replaced. Declared
  uninstall hooks are unaffected by `--install-hook`.
- `HOOK-57` `init-source` scaffolds commented `[[hooks]]` examples in the
  `mind.toml` it writes: at least one install hook and one uninstall hook, with
  one marked `optional = true`, each showing `run`, `name`, and `event`, all
  commented out so they are inert until the maintainer fills them in. The
  `optional` example's comment states that optional lets the user decline running
  the hook (it does not mean the hook may fail).
- `HOOK-58` `recall --sources` marks a source carrying install hooks with a
  count-aware token; `mind review <target>` lists every declared hook (install and
  uninstall), showing each hook's command, event, and whether it is required or
  optional (extending HOOK-40).
- `HOOK-59` `unmeld --uninstall-hook <cmd>` supplies or overrides a source's
  uninstall hook: it replaces all the source's declared uninstall hooks with one
  required uninstall hook running `<cmd>`, shown loudly in the disclosure (the
  uninstall-event counterpart to `meld --install-hook`, HOOK-56). Declared install
  hooks are unaffected.

## Deprecating `[source].install`

- `HOOK-90` `[source].install` (HOOK-1) is deprecated in favor of a `[[hooks]]`
  install entry (HOOK-50/51), which is strictly more expressive (it can be named,
  optional, an uninstall hook, or one of several). The field stays parsed and
  folded into the hook set (HOOK-50), so existing sources keep working unchanged;
  only new authoring is steered away. `mind review <target>` reports a source that
  declares `[source].install` as an advisory `deprecated-field` finding, naming the
  equivalent `[[hooks]]` entry (`run = <cmd>`, `event = "install"`), in addition to
  disclosing the hook itself (HOOK-40). `init-source` never writes `[source].install`:
  its scaffold offers only commented `[[hooks]]` examples (HOOK-57).

## Item build hooks

Source-level hooks (HOOK-50) run once in the clone at `meld`/`unmeld`. Tooling
that an individual item ships and must build before use (a compiled binary, a
vendored dependency) instead uses an item-level build hook: a command tied to one
item that runs when that item is installed, in the item's staging copy, so its
output is captured into the store transactionally. Build hooks back the `tool`
kind and `{{tools:name}}` (tooling.md, TOOL-12), but any kind may declare one.

- `HOOK-70` An item declares a build hook with `build`, a shell command:
  `[[items]].build` in `mind.toml`, or `build:` in a tool's `TOOL.md`
  frontmatter. It is distinct from a source's install/uninstall hooks (HOOK-50):
  it is item-scoped and runs per item at install, not once per source at meld. An
  empty or whitespace-only `build` is treated as absent (HOOK-3).
- `HOOK-71` A build hook runs in the item's staging directory
  (`~/.mind/.tmp/staging/<kind>/<name>/`) as the working directory, after
  reference/token expansion (NS-11, TOOL-13) and before the store swap (LIFE-1),
  so its output lands in the store atomically on success. A non-zero exit is a
  hard stop (HOOK-53): the staging copy is discarded and the live install is left
  untouched (LIFE-4), as for any failed install.
- `HOOK-72` A build hook is arbitrary code, so it is disclosed before running and
  its output is framed (HOOK-30). On a TTY it is prompted two-way: run it, or skip
  it and install the item with its tooling unbuilt (`mind` says so, HOOK-21, and a
  `{{tools:ref}}` then points at an unbuilt path until the build runs). A non-TTY
  context skips it (the item installs unbuilt). Skipping is the graceful decline;
  a build hook offers no abort, so a single item's build never aborts a batch
  install.
- `HOOK-73` A build hook re-runs whenever its item is (re)installed or upgraded,
  since the store copy is rebuilt from staging each time. `learn`/`upgrade`
  disclose and prompt for it as part of installing the item; nothing
  beyond the item's content hash is recorded for it.
- `HOOK-74` `--dangerously-skip-build-hook-check` on `learn`, `upgrade`, `meld`
  (for its install-all path), and `sync --upgrade` runs an item's build hook
  non-interactively (no prompt, treated as approved), enabling build hooks in CI
  and headless scripts. Without it, the HOOK-72 behavior is unchanged: a non-TTY
  context skips the build hook and prints a note. The flag name mirrors
  `--dangerously-skip-install-hook-check` (HOOK-23) and is deliberately explicit
  about the risk. A non-zero exit from the build hook under this flag is still a
  hard stop: the staging copy is discarded and the install is left untouched
  (HOOK-71). Approving the run headlessly does not waive the failure contract.

## Item install and uninstall hooks

Status: done. A `build` hook (HOOK-70) produces an item's store content in
staging. Separately, an item may need a side-effect command on the host when it
is installed (set up a venv, register a helper) and a matching cleanup command
when it is removed. These are the item-level analog of a source's install and
uninstall hooks (HOOK-50), scoped to one item and tied to its install/removal
rather than to `meld`/`unmeld`. They are distinct from `build`: `build` runs in
staging before the store swap and produces content; an install hook runs after
the item is in place and is for host side effects.

- `HOOK-80` An item declares lifecycle hooks with `install` and `uninstall` shell
  commands: `[[items]].install` / `[[items]].uninstall` in `mind.toml`, or
  `install:` / `uninstall:` in a tool's `TOOL.md` frontmatter. They are valid on
  any kind and are distinct from `build` (HOOK-70). An empty or whitespace-only
  value is treated as absent (HOOK-3). (HOOK-86 adds a `[[items.hooks]]`
  array form for multiple/named/optional item hooks; these scalars are then its
  shorthand.)
- `HOOK-81` An item's install hook runs as the final step of installing the item:
  after its store copy is swapped in and its links are created (LIFE-1), with the
  working directory set to the item's installed store location (the item directory
  `~/.mind/store/<kind>/<name>/` for directory-backed kinds like skill and tool;
  the parent `~/.mind/store/<kind>/` for single-file agent and rule items, which
  have no `<name>/` directory), and after the item's `build` hook (which runs
  earlier, in staging).
  It runs at `learn` and re-runs whenever the item is reinstalled or upgraded
  (`upgrade`), so the side effect tracks the item's content, mirroring the build
  hook's re-run rule (HOOK-73). A non-zero exit is a `HookFailed` hard stop that
  rolls back that item's install (its store copy and links are removed, leaving it
  not installed); host side effects the hook already performed are outside `mind`'s
  state and are not rolled back (HOOK-30).
- `HOOK-82` An item's uninstall hook runs when the item is removed: at `forget`,
  at `unmeld` (which removes the source's items), and for the OLD item when an
  `upgrade` renames it (a namespace change removes the old item via its registry).
  It runs with the same working directory as the install hook (HOOK-81) before the
  store copy and links are removed, so cleanup can use the installed files. It does NOT run on an in-place
  upgrade (same effective name, content swapped), since the item is not removed,
  only its install hook re-runs (HOOK-81). A non-zero exit is a hard stop
  (HOOK-53): the removal stops and the item is left installed, mirroring a failed
  source uninstall hook leaving the source melded (HOOK-54).
- `HOOK-83` Item install and uninstall hooks are arbitrary code, so each is
  disclosed and its output framed exactly as other hooks (HOOK-20, HOOK-30). On a
  TTY each is prompted two-way (run / skip), like the build hook (HOOK-72): for an
  install hook, skipping installs the item without the side effect; for an
  uninstall hook, skipping removes the item without running cleanup. There is no
  abort, so one item's hook never aborts a multi-item `learn`/`forget`. A non-TTY
  context skips them, and `--dangerously-skip-install-hook-check` runs them
  unattended (HOOK-22, HOOK-23).
- `HOOK-84` Nothing beyond the item's content hash is recorded for an item's
  install/uninstall hooks (as for `build`, HOOK-73): the install hook re-runs on
  every (re)install or upgrade of the item, and the uninstall hook fires on every
  removal. Together with the source-level rule (a source's install hooks re-run
  when the source advances, HOOK-11/55), an install hook re-runs whenever its
  source OR its item is upgraded.
- `HOOK-85` `mind review <target>` lists an item's declared install and uninstall
  hooks as advisory findings, showing each command and event, so a consumer sees
  before installing that an item will run code (the item-level counterpart of the
  source-hook disclosure, HOOK-40/58).
- `HOOK-86` An item may declare lifecycle hooks as an array, `[[items.hooks]]` in
  `mind.toml`, with the same fields and semantics as a source's `[[hooks]]`
  (HOOK-51): `run`, `name`, `optional`, and `event` (`"install"` | `"uninstall"`),
  run in declaration order at the item's install/removal. This is the per-item
  analog of the source `[[hooks]]` array (HOOK-50), giving items full parity:
  multiple, named, and optional hooks per event. The scalar `[[items]].install` /
  `[[items]].uninstall` (HOOK-80) remain as a shorthand for one required hook of
  each event, folded in ahead of any `[[items.hooks]]` entries (the item-level
  mirror of HOOK-50's fold-in of `[source].install`). In a tool's `TOOL.md`
  frontmatter the scalar `install:` / `uninstall:` keys stay the only form, since
  the frontmatter reader is scalar-only (DSC-21); the array form requires
  `mind.toml`. Each `[[items.hooks]]` entry is disclosed, prompted, recorded, and
  fails exactly as the scalar item hooks do (HOOK-81..84).
- `HOOK-87` Install and uninstall hooks nest: a source is the outer scope, its
  items the inner. Setup runs outer-to-inner and teardown inner-to-outer. On a
  `meld` that installs, the source's install hooks (HOOK-50) run first, then each
  item installs with its `build` (HOOK-71) and install hooks (HOOK-81). On
  teardown the order reverses: each item's uninstall hooks (HOOK-82/86) run before
  the source's uninstall hooks (HOOK-54). This reorders the current `unmeld`
  behavior, where the source's uninstall hooks run before the items'; under
  HOOK-87 the source's uninstall hooks run last, in the clone, after every item
  has been removed and before the clone and registry entry are dropped (the
  multi-item confirmation, CLI-42, still gates all of it). The end-to-end flow is
  `source.install -> item.install* ... item.uninstall* -> source.uninstall`.

## Running hooks on demand (`mind hooks run`)

Status: done. Hooks normally run as a step of another verb: install hooks at
`meld`/`upgrade` (HOOK-10/11), uninstall hooks at `unmeld` (HOOK-54), and item
hooks at `learn`/`forget`/`upgrade` (HOOK-73/81/82). That leaves no way to run a
hook the user earlier skipped, to re-run a hook whose effect was later lost (a
deleted build output or side effect), or to re-run an install or uninstall whose
prior run failed for a transient reason -- short of a full re-meld or reinstall.
`mind hooks run` fills that gap: it runs a source's or an item's hooks on demand,
outside the surrounding verb, under the same consent model.

- `HOOK-100` `mind hooks run <target>` executes hooks on demand, outside the
  meld/learn/forget/upgrade flows. It reuses those flows' machinery unchanged: the
  disclosure (HOOK-20, sanitized per HOOK-91), the consent prompt (three-way for a
  required hook, two-way for an optional, build, or item hook, HOOK-52/72/83), the
  non-TTY skip (HOOK-22/72/83), and the `--dangerously-skip-install-hook-check` /
  `--dangerously-skip-build-hook-check` bypasses (HOOK-23/74). It is therefore
  neither more nor less guarded than an automatic run. A hook's non-zero exit is a
  hard stop with the same contract as an automatic run (HOOK-53); a required hook's
  failure or abort is a non-zero exit for the command.
- `HOOK-101` A source target runs the source's `[[hooks]]` for the selected event
  (`--event`, CLI-195; default `install`) in declaration order in the clone
  (HOOK-50/54). For the install event, by default only *pending* install hooks run
  -- a hook that never ran or whose recorded run-commit differs from the source's
  current commit (HOOK-55) -- so a plain `mind hooks run <source>` runs exactly the
  install hooks a skip or an added hook left outstanding. `--force` runs every
  install hook of the source regardless of its recorded commit (the lost-output or
  transient-failure case), mirroring `meld --force` (HOOK-60). Each install hook
  that runs updates its recorded run-commit exactly as an automatic run does
  (HOOK-55); uninstall hooks stay unrecorded (they fire only here and at `unmeld`).
- `HOOK-102` An item target (`<source>#<item>`) runs that item's install or
  uninstall hooks (HOOK-80/86) for the selected event, in place: with the working
  directory set to the item's installed store location (HOOK-81), against the
  current store copy, with no reinstall. For the install event, `hooks run`
  offers only the item's *pending* install hooks (HOOK-110): a hook already
  recorded as run at the item's current commit is filtered out before it is
  offered, mirroring HOOK-101's source-side filter. Uninstall hooks carry no
  recorded run-state (HOOK-84) and always run. `hooks run` requires the target
  item to be installed; running against an item that is not installed is an
  error naming it (there is no store copy to run against).
- `HOOK-103` `--event build` is valid only for an item target and re-runs the
  item's build hook (HOOK-70). Because a build hook is staging-bound and its output
  must land in the store transactionally (HOOK-71), `hooks run --event build`
  re-installs the item through the normal transactional path (stage, expand, build,
  swap; LIFE-1/LIFE-4) rather than running the build command against the live store
  copy, so a failed rebuild leaves the existing copy untouched. This is the
  supported way to rebuild an item whose build output was deleted or corrupted.
- `HOOK-104` `mind hooks list <target>` reports the hooks in effect for a source
  (and its installed items) without running any: per hook, its event, its
  required/optional flag, its command, and -- for a recorded source install hook
  (HOOK-55) -- whether it is pending and the commit it last ran at. It is the
  read-only companion to `hooks run` and the detail view behind the `recall
  --sources` hook marker (HOOK-58).
- `HOOK-105` Before applying the `#`-split heuristic (source selector vs.
  `<source>#<item>` item ref), `hooks run`/`hooks list` check whether the whole
  target string, trimmed, exactly matches a registered source's identity. When
  it does, the target is a source target even though it contains `#`. This
  covers an item-link instance (LNK-4), whose own identity is
  `host/owner/repo#<path>`: without this check the exact identity parses as an
  item ref that matches no installed item (`NotInstalled`), leaving the
  instance's source-level hooks reachable only through an over-broad trailing
  glob. A target that does not exactly match any registered source identity
  keeps the ordinary `#`-split behavior: a plain `source#item` resolves as an
  item ref, and so does the `<link-identity>#<item>` form for an item installed
  from a link instance (the identity's own embedded `#` composes with the
  item-ref split, since the split is on the LAST `#`).

  A `#`-carrying target string can exactly match a registered source's
  identity AND, under the ordinary `#`-split reading of that same string, also
  name an installed item -- e.g. a link instance whose linked skill sits at a
  single top-level path segment `foo` has identity `host/owner/repo#foo`,
  spelled identically to item `foo` in a plain-melded source
  `host/owner/repo`. Which reading is meant then depends on registry state
  invisible to the caller (whether that link instance happens to be melded),
  so resolving it silently either way would change a script's meaning as a
  side effect of an unrelated meld. `hooks run`/`hooks list` therefore report
  this specific case as `AmbiguousHookTarget`, naming the ambiguous string and
  the item(s) it also matches, rather than picking the source (or the item) as
  a winner. Two escapes disambiguate explicitly and always take priority over
  both the exact-identity check and the ambiguity check, so they work whether
  or not a given target happens to collide:
  - A leading `source:` prefix (e.g. `source:host/owner/repo#foo`) always
    forces the source reading; the prefix is stripped before the rest of
    target resolution runs.
  - A kind-qualified item ref (`<source>#<kind>:<name>`, e.g.
    `host/owner/repo#skill:foo`) always forces the item reading: it never
    equals a registered source's exact identity (an identity carries no
    `kind:` segment), so it falls through to the ordinary `#`-split item-ref
    parsing unconditionally.

  A target with no `#` at all is never ambiguous: the CLI-194 heuristic reads
  it as a source selector unconditionally, so it is never read as an item ref
  in `hooks run`/`hooks list` regardless of registry state.
- `HOOK-106` When `hooks run` skips a source hook because the run is non-TTY
  (HOOK-22) rather than because a user interactively declined it, the skip
  note names both the cause and the exact remedy instead of stating only the
  bare consequence: `note: skipped <event> hook '<label>' for <source> (not a
  terminal); re-run unattended with:` followed, on its own indented line, by
  `mind hooks run '<source>' --event <event>
  --dangerously-skip-install-hook-check`. `<source>` is
  substituted with the RESOLVED source identity, never the raw selector the
  user typed (which may be a glob or an abbreviated form), and `<event>` with
  the event the run selected, so the printed command is copy-pasteable with no
  placeholder to fill in and re-runs the hooks that were skipped. The `<source>`
  identity inside the runnable command is single-quoted (the POSIX `'\''` idiom)
  by exactly the same rule and for exactly the same reason as the single-match
  `HooksNotRun` remedy described below: the note's runnable command carries the
  same attacker-influenced identity, so copy-pasting it must not be a way to
  run anything beyond `mind hooks run`. The runnable command is printed on its
  own line with no surrounding shell-quote character -- earlier revisions
  framed it in double quotes, but a double-quote presentation frame is itself
  copyable: pasting the frame's own `"` characters together with the
  (already single-quoted) identity keeps `$`/backtick special inside a
  double-quoted context, so a `$(...)`/backtick payload inside the identity
  could fire again if the frame were included in the paste even though the
  identity is safely single-quoted on its own. Putting the command on its own
  line removes any shell-quote character from the presentation entirely, so a
  verbatim paste of the whole remedy -- frame included -- stays inert. The bare
  `for <source>` earlier in the note is English prose, not a shell command, so
  it is not quoted. Every other runnable `mind hooks run <arg>` example `mind`
  prints for this same target family -- including `AmbiguousHookTarget`'s
  (HOOK-105) two disambiguating examples (`mind hooks run 'source:<target>'`
  and the kind-qualified item ref example) -- shell-quotes its identity the
  same way; none of the family's printed remedies interpolates an identity bare.
  The
  `--event` segment is not optional dressing: `--event` defaults to `install`
  (CLI-195), so a remedy that omitted it would, after an `--event uninstall`
  run, silently name a command that executes the source's install hooks
  instead. An interactive decline (the user is on a TTY and answered no/skip)
  keeps the plain `skipped ... hook ... for ...` note: there is no "cause" to
  name when the user made an active choice.

  The `HooksNotRun` message (HOOK-107/HOOK-108) aggregates across every
  matched source or item, so it cannot always follow the same one-remedy shape
  as the per-hook note above: when exactly one source or item contributed a
  skipped hook, the message substitutes that one resolved identity and carries
  the identical paste-able remedy; when a selector matched SEVERAL sources or
  items (a glob) that all contributed, no single command is synthesized from
  the raw selector -- doing so for a selector like `'*'` would print an
  unquoted shell glob that expands against the caller's cwd if pasted, naming
  something that may not even be a source. Instead the message lists every
  resolved identity that had work, so the reader substitutes one at a time; it
  never echoes the raw multi-match selector back into a command-shaped string.
  An item ref that itself resolved to exactly one item, and carries no glob
  metacharacter (`*`, `?`, `[`), keeps that exact typed form in its
  single-target remedy (whatever form it was spelled in -- bare, a
  kind-qualified escape, or an abbreviated source selector), since rewriting
  it into a different form risks landing back in the HOOK-105 source/item
  ambiguity a kind-qualified escape exists specifically to avoid. An item ref
  that resolved to exactly one item but DOES carry a glob metacharacter (e.g.
  `agents#skill:sc*`, which happens to match only one installed item) does
  NOT keep its typed form for this reason: the glob text itself is not safe to
  paste into a shell (it would expand against the caller's cwd rather than
  necessarily naming the item), so it is replaced with the resolved concrete
  `<source>#<kind>:<name>` identity exactly as the multi-match case is, even
  though only one item is being named.

  Every resolved identity the `HooksNotRun` message substitutes into a
  runnable command -- whether the single-match remedy or the parenthetical
  list in the multi-match message -- is single-quoted (the POSIX `'\''` idiom
  for an embedded quote) before it is placed there, unconditionally, whether
  or not it happens to contain a shell metacharacter. A source name, a `meld
  --as`/`[source].prefix` alias, and an item-link's own `#<path>`-suffixed
  identity are none of them restricted against characters a shell treats
  specially (`;`, `$(...)`, a backtick, whitespace, a quote), so pasting an
  unquoted identity into the remedy would let it run more than `mind hooks
  run`. This is what makes the promise above ("copy-pasteable with no
  placeholder to fill in") also mean "and copy-pasting it is not itself a way
  to run something else": quoting closes that gap regardless of which kind of
  identity ended up in `resolved`. The single-match remedy puts the runnable
  command on its own indented line after a "re-run unattended with:" lead,
  with no surrounding shell-quote character, for the same paste-safety reason
  given under HOOK-106 above: a quote-character presentation frame is itself
  copyable, and pasting it along with the (already single-quoted) identity can
  re-expose a metacharacter the identity's own quoting had already neutralized.
- `HOOK-107` `hooks run <target>` on a source target counts, across every
  matched source, how many hooks for the selected event EXISTED (were
  actually considered: not filtered out as already-up-to-date at the current
  commit, HOOK-101), how many of those RAN, and how many were SKIPPED
  specifically for want of consent (the HOOK-106 non-TTY case, not an
  interactive decline). When at least one hook existed, none ran, and at
  least one was skipped for want of consent, `hooks run` returns
  `MindError::HooksNotRun` (naming the target and the skipped count) instead
  of the silent exit 0 a non-TTY invocation previously produced -- the
  provisioning-script case U43 identifies, where "ran nothing" and "skipped
  everything because there was no terminal to ask" are indistinguishable
  under a bare exit code. A run with nothing to do -- no hooks declared for
  the event, or every install hook already recorded at the current commit --
  is unaffected and stays exit 0: only the specific "there was work, and
  consent was unavailable for all of it" case is an error. A required hook's
  interactive abort (HOOK-100) is unaffected; it already reports
  `HookAborted`. This also gives `hooks run --json` a structured result on the
  no-op path (CLI-181's error envelope), where it previously produced only an
  unstructured `note: skipped <event> hook '<label>' for <source>` line at exit
  0.
- `HOOK-108` The HOOK-107 accounting applies to an item target
  (`<source>#<item>`) as well: `hooks run --event install|uninstall` counts,
  across every item the ref matched (CLI-194), how many of that item's hooks
  for the selected event existed, how many ran, and how many were skipped for
  want of consent, and reports `MindError::HooksNotRun` under the same
  predicate (at least one existed, none ran, at least one was skipped for want
  of consent). For the install event, "existed" excludes any hook already
  recorded as run at the item's current commit (HOOK-110): a hook filtered out
  that way was never offered, so it counts toward neither "existed" nor "ran"
  nor "skipped". The provisioning-script case U43 describes does not care
  whether the target was spelled as a source or as one of its items, so
  neither does the report. The error names the target as typed and carries the
  same remedy as HOOK-106. An item with no hooks declared for the selected
  event, or whose install hooks are all already recorded as run at the current
  commit, stays exit 0, and an interactive decline stays exit 0, exactly as for
  a source target. `--event build` is outside this accounting: it re-installs
  the item through the transactional path (HOOK-103), where the build hook's
  consent outcome is a step of the install rather than a result reported back
  to `hooks run`.
- `HOOK-110` An installed item records its install hooks as a set
  (`InstalledItem.install_hooks`, STO-75), the per-item counterpart of a
  source's `install_hooks` record (HOOK-55): each entry is an effective
  install-hook command plus the commit it last ran at, or absent (`None`) when
  it was offered and skipped rather than run. The record is written by every
  path that runs an item's install hooks -- `learn`, `upgrade`'s reinstall
  cycle, and `hooks run` -- not just `hooks run`, so a hook `learn` skipped
  (non-TTY) or ran (`--dangerously-skip-install-hook-check`) is reflected
  immediately, before any `hooks run` of that item. `learn` and `upgrade`
  build the record fresh on every (re)install (HOOK-84 is unaffected: they
  still run or offer every declared install hook regardless of any prior
  record); only `hooks run` reads the record, to filter its HOOK-108
  accounting down to pending hooks (HOOK-102), UNLESS `--force` is given, in
  which case an item target skips the filter exactly as a source target does
  (HOOK-101): every declared install hook is offered regardless of its
  recorded commit, for the same lost-output/transient-failure case `--force`
  exists for on the source side. This closes the gap where a
  provisioning script doing `mind learn x --dangerously-skip-install-hook-check
  && mind hooks run src#x` failed on the second command forever, with a remedy
  that re-runs a side effect the first command already applied. Because
  `learn`/`upgrade` rebuild the record from scratch on every (re)install rather
  than merging it with what was there before, a record tied to an old commit
  never suppresses a hook at a new one, and `forget` (which removes the whole
  manifest entry) removes the record along with the item. When a `hooks run`
  batch aborts partway through (HOOK-53: a hook's non-zero exit is a hard
  stop), the hooks that already ran or were offered before the failing one
  keep their persisted records rather than being discarded with the batch,
  mirroring the source-level record's same guarantee (HOOK-101/HOOK-55): the
  save happens before the error propagates, so a retry after fixing the
  failing hook does not re-offer (and, under
  `--dangerously-skip-install-hook-check`, re-run) a sibling hook that already
  applied its side effect. This applies only where the item stays installed
  regardless of the batch's outcome, i.e. `hooks run`; on the `learn`/
  `upgrade` install path a hook's failure rolls the whole just-installed item
  back (HOOK-86), so there is no manifest entry left to attach a partial
  record to and nothing is persisted for that path.
- `HOOK-109` The interactive-terminal test (HOOK-22's gate, `hook::is_tty`)
  reads `$MIND_TTY` before falling back to inspecting stdin. When the variable
  is set, its value alone decides: `0`, `false`, `no`, `off`, and the empty
  string (any case, surrounding whitespace ignored) mean "not a terminal", and
  any other value means "a terminal". This follows the `MIND_HOME` /
  `CLAUDE_HOME` / `MIND_DETECT_HOME` test-isolation precedent (STO-1, HARN-5)
  and exists for the same reason: without it, every consent path that turns on
  being interactive (the HOOK-20/HOOK-52 prompts, the HOOK-106 distinction
  between a non-TTY skip and an interactive decline) is unreachable from a
  headless test, so the branch that decides whether arbitrary source code runs
  is the one branch nothing verifies. The override changes only which branch is
  taken; it never bypasses a prompt or consents on the user's behalf, and a
  prompt that reaches EOF still skips (HOOK-22).

## Managed-policy composition (research needed)

Install hooks are arbitrary code execution, which is exactly what an enterprise
managed policy (policy.md) exists to control, so the two compose. This section is
NOT yet specified: it records the design space and open questions to research
before any normative rule or stable ID is added. The default in the meantime is
the unmanaged behavior above (prompt, default No; non-TTY skips per HOOK-22).

Open questions to resolve:

- Stance. Should a policy forbid hooks outright (refuse to meld a source that
  declares one, or always skip hooks), allow them with the prompt unchanged, or
  pre-approve a specific set? A locked-down org likely wants "forbid" or
  "pre-approve", not "prompt".
- Pre-approval shape. If pre-approving, what is the unit: a source identity plus an
  expected exact command, plus a pinned commit, so a hook runs unattended only when
  it matches the approved (source, command, commit) triple and is refused otherwise?
- Bypass control. Should a policy be able to disallow
  `--dangerously-skip-install-hook-check` (force the prompt, or forbid running
  hooks at all), so a user cannot opt out of the policy's stance?
- Non-interactive provisioning. `auto_meld` (POL-32) runs during `sync` with no
  TTY, so a declared hook is skipped today (HOOK-22). This is partly resolved for
  build hooks: an `auto_meld` entry with `install = true` (POL-58) installs the
  source's items during provisioning, with build hooks skipped by default and
  opted in per entry via `run-build-hooks = true` (POL-59). Install hooks remain
  skipped per HOOK-22 (a policy provisioning a source whose tooling depends on an
  install hook still leaves that tooling unrun); pre-approving install hooks for
  unattended runs is the open part of this question.
- Audit. Whether a managed deployment should record each hook execution (source,
  command, commit, and how it was approved) for compliance.

The crux is the threat model: a hook is the most dangerous surface mind has, so the
policy's relationship to it should be deliberate. Resolve these before assigning
stable IDs and folding the rules into policy.md.