mind-cli 0.48.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
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
# 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); `--accept-hooks` 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 hook's
  lifecycle event (HOOK-120), 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. The event is named because the block is otherwise identical across
  events: without it, approving an update or uninstall hook looks exactly like
  approving an install hook, on the one surface where the answer decides whether
  arbitrary source code runs. 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, or EOF, CLI-258) is
  run; an unrecognized reply is rejected and asked again (CLI-257), so an
  unclear answer never runs the hook. Where a verb asks a unified plan
  (HOOK-180, HOOK-186), this prompt appears only for a hook the plan did not
  list (HOOK-185) or when the user chose to review each hook (`r`, HOOK-181). 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
  `--accept-hooks` accepts the hook's event (HOOK-150).
- `HOOK-23` `--accept-hooks` runs the hook without prompting, and is what
  enables hooks in non-interactive use. Bare, it accepts every hook;
  `--accept-hooks=<events>` accepts only the listed events (HOOK-150, CLI-250).
- `HOOK-150` `--accept-hooks` takes an optional comma-separated list of hook
  events: `install`, `update`, `uninstall`, `build`. The flag is repeatable and
  the lists union. A hook runs unattended only when its own event is listed: an
  `install` hook (source or item) needs `install`, an `update` hook `update`, an
  `uninstall` hook `uninstall`, and an item build hook (HOOK-70) `build`. A hook
  whose event is not listed takes the ordinary path (prompt on a TTY, HOOK-22
  skip without one). So `upgrade --accept-hooks=install` re-runs a source's
  pending install hooks but not its update hooks (HOOK-122).
- `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).
  A unified plan (HOOK-180) shows the same browse URL for each source hook it
  lists, and for an item hook the clone path to inspect (HOOK-184).
- `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 INHERITED
  (unless output logging is on, HOOK-165, which copies them through `mind` as
  they arrive instead), so its output reaches the terminal as it is produced rather than being
  collected and replayed when it exits. A hook is often the slowest step of a
  meld (a build, a package install); output that arrives only at the end tells
  the user nothing while they wait, and gives them nothing to interrupt on.
  The run is framed by `====== (hook: <name>) ======` before the spawn and
  `====== (end hook: <name>, <status>) ======` after it (HOOK-162), so the hook's output stays
  demarcated from `mind`'s own and from whatever it prints next (e.g. the
  install preview). The frame itself is printed on `mind`'s stdout only, never
  stderr, so `mind meld > /dev/null` discards both frame lines even though the
  hook's own inherited stderr still reaches the terminal, unframed -- an
  asymmetry the streaming design does not close, since framing stderr the same
  way would mean buffering it, the same cost the emptiness-detection
  discussion below rules out. Two consequences follow from streaming and are
  intended: the two streams interleave exactly as they would in a terminal, so
  there are no longer separate `hook-stdout`/`hook-stderr` blocks, and the
  frame is unconditional, since nothing is buffered and `mind` cannot know in
  advance whether the hook will print anything. A third consequence is the
  visible behavior change: a hook's stderr now lands on `mind`'s STDERR
  instead of being replayed onto stdout, so each stream keeps its identity and
  a caller's redirection means what it says (`2>/dev/null` silences a noisy
  hook's warnings, and a hook's diagnostics no longer contaminate a stdout
  pipe). 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 at the frame already on screen rather than
  repeating its contents, since nothing was captured to repeat. It points
  there WITHOUT asserting that output exists ("its output, if any, is in the
  frame above"): inherited streams mean `mind` never saw a byte, so it cannot
  distinguish a silent hook from a talkative one, and a message claiming
  either would be wrong half the time. Detecting emptiness would require
  piping the streams through `mind`, which costs the hook its terminal:
  `isatty` turns false, so tools drop color and progress rendering, and C
  stdio switches from line buffering to 4 KiB blocks, delivering a long
  build's output in bursts. That trade loses more than the wording gains. A
  hook that fails to SPAWN is the exception: it never ran, so that error
  carries the spawn failure's own reason instead. 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.
  The message names the hook's EVENT (`install`, `uninstall`, `build`), which
  the call site fixes, so a failing uninstall or build hook is never reported
  as an install one. When the frame's heading differs from the event it is
  named too, quoted, so the error points at the frame the user actually saw:
  a source hook is headed by its author's `[[hooks]].name`, or by the command
  itself when it declares none (HOOK-51), while a per-item lifecycle hook's
  heading already IS its event and is not repeated.
- `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.
- `HOOK-32` Streaming is safe under `--json` (CLI-217) without a special
  case, on unix (the redirect is `#[cfg(unix)]`; a non-unix build performs no
  redirect and relies on discipline instead): the whole run executes with fd
  1 pointed at fd 2 by a real `dup2`, and an inherited child writes to that
  same redirected descriptor, so a hook's output cannot reach the result
  document on stdout. The descriptor `--json` saves aside to write that
  document once the run succeeds (`REAL_STDOUT` in main.rs's `json_stdout`)
  is itself opened close-on-exec (`F_DUPFD_CLOEXEC`, not a plain `dup`), so a
  hook cannot reach the real stdout through an inherited copy of that
  descriptor either (e.g. `>&3`, the lowest fd `Command` leaves unrewired):
  only `mind`'s own process keeps it open, and a child's exec closes its copy
  before the hook's own code runs. The same holds for the TUI, which
  redirects both stdout and stderr by the identical close-on-exec dance while
  an action runs (`tui/action.rs`'s `with_captured_stdout`).

## Hook results

Every hook `mind` considers produces a result record. The records reach the
caller in the run's `--json` document, persist in a capped history file, and
can point at a saved copy of the hook's output.

- `HOOK-160` Each hook considered in a run (source or item, any event,
  including `build`) produces one record:

  ```json
  {"at": "2026-10-28T14:03:09Z", "run": "20261028T140301Z-4242",
   "verb": "meld", "event": "install", "label": "setup",
   "source": "github.com/owner/repo", "item": "skill:scan",
   "command": "make setup", "commit": "<sha>", "outcome": "ok",
   "exit_code": 0, "signal": null, "duration_ms": 2100, "log": null}
  ```

  `outcome` is `ok` (exited 0), `failed` (exited non-zero, killed by a
  signal, or could not be started; the last carries an `error` member with
  the reason), `skipped` (not run for want of consent: no terminal and no
  `--accept-hooks` for its event), `declined` (the user was asked and said
  no), `aborted` (the user aborted at a required hook's prompt), or
  `interrupted` (HOOK-171). Where an
  abort does not stop the operation (the `upgrade` re-run of HOOK-11, which
  treats it as a skip), the hook is recorded as `declined`. `label` is
  the heading of the hook's frame (HOOK-30): a source hook's name, else its
  command (HOOK-51), and an item hook's event. `item` is present only for an
  item hook. A consumer's override (HOOK-56, HOOK-59) carries a `replaced`
  array, the declared commands its `command` ran in place of; other records
  omit it. `exit_code`, `signal`, and `duration_ms` are
  `null` for a hook that did not run. `at` is the time the hook finished or
  was passed over, in UTC. `run` is shared by every record of one `mind`
  process; `verb` names the command that considered the hook. A hook the
  pending filter holds back (HOOK-60, HOOK-101, HOOK-110), or one a managed
  policy's locked allowlist keeps from running (POL-12), is not considered
  and has no record. `duration_ms` runs from the spawn to the hook's exit.
  Every source-controlled string is sanitized (DSC-95).
- `HOOK-161` Under `--json`, the run's result document carries the run's
  records, in order, as a top-level `hooks` array when at least one hook was
  considered, and has no `hooks` member otherwise. A run that fails carries
  the same array on its error envelope (CLI-181), the failing hook last, so a
  caller sees which hook failed and its exit code without parsing the
  message.
- `HOOK-162` The closing frame line (HOOK-30) names how a hook that ran
  exited: `====== (end hook: <name>, exit 0, 2.1s) ======`, or `signal <n>`
  in place of the exit code. A run under a second is shown in milliseconds
  (`exit 0, 12ms`). A hook that could not be started keeps the bare
  `====== (end hook: <name>) ======`, since it has no status.
- `HOOK-163` Each record is also appended to `~/.mind/hooks/history.jsonl`,
  one JSON object per line, oldest first, as soon as the hook finishes, so a
  run that stops partway still leaves the hooks it reached on record,
  including one interrupted with Ctrl-C (HOOK-171). The file
  keeps the newest `hook-history` records (default 100) and is rewritten
  atomically on each write, readable by its owner only (mode 0600 on Unix);
  lowering the cap trims the file on its next
  write. A line that does not parse is dropped on the next write rather than
  failing the run. A failure to write the history
  warns once on stderr and never fails the hook or the verb.
- `HOOK-164` The `hook-history` config key sets the cap; `0` turns the
  history off: nothing is written, and `hooks history` says so, while still
  showing any records an earlier file holds. A `config.toml` that does not
  parse leaves every hook setting at its default (history on at 100, no
  logs); when the file names a hook key (`hook-history`, `hook-log`,
  `hook-log-keep`), `mind` warns on stderr that it was not applied.
- `HOOK-165` Output logging saves each hook's output to a file while still
  showing it. `--hook-log` (CLI-252) turns it on for one run; the `hook-log`
  config key makes it the default (`true` for the owned directory, a path for
  a directory of one's own, `false` or absent for off; any other value is a
  config error naming what the key takes), and `--no-hook-log` (CLI-256)
  turns it off for one run. An empty `hook-log` path names no directory and
  means the owned one; a relative one is relative to `~/.mind`, the
  directory holding `config.toml`. With logging on, the
  hook's stdout and stderr are piped through `mind` instead of inherited:
  each chunk is written to `mind`'s own stdout or stderr as it arrives, so the
  output still streams and each stream keeps its channel, and copied to
  `<dir>/<run>/<seq>-<event>-<target>.log`, where `<target>` is the source and
  the hook's name for a source hook, and the source and item for an item hook
  (each made file-name safe). The log is headed by the hook's event, label (a
  source hook's only), target, and command, each line tagged `out | ` or
  `err | ` in arrival order (a line longer than 64 KiB is split, each piece
  tagged, so output with no newline is not held in memory), and ended with the exit status (`# exit <n>`, `# signal <n>`, or
  `# not started: <reason>`). Each log is created new and readable by its
  owner only (mode 0600 on Unix): a file or symlink already at its path is
  never opened, so the log cannot be created (below). The relative order of the two
  streams is only as exact as two pipes allow. The cost is the one HOOK-30
  names: a piped hook does not see a terminal, so tools drop color and
  progress rendering. A background process the hook leaves writing to those
  pipes also loses its reader when `mind` exits, so its later writes fail
  (`SIGPIPE`) rather than reaching the terminal. That is why logging is
  opt-in. The record's `log` names the file, as an absolute path (a relative
  `--hook-log` directory is resolved against the working directory at
  startup), and
  `mind` prints a note naming it after the frame. A hook that keeps the pipes
  open past its exit delays `mind` by at most two seconds; output after that
  still reaches the log, after its exit line. A log that cannot be created
  warns and the hook runs unlogged.
- `HOOK-166` A log under `~/.mind/hooks/logs` is owned by `mind`, including
  one written there because a named directory lies inside it: when its record
  is trimmed from the history, the log is deleted, and its run directory once
  empty. Whether a log is owned is decided by its path, with `..` resolved,
  and only a `.log` file is ever deleted, so a `log` value edited in the
  history file cannot make eviction delete anything else. A log in any other
  directory is never deleted. With the history off (HOOK-164) nothing is
  trimmed, so instead a run's first log under `~/.mind/hooks/logs` deletes
  each owned `.log` file there last modified more than 30 days ago, and each
  directory below it that leaves empty, following no symlinks.
- `HOOK-167` `mind hooks history` (CLI-253) prints the history, filtered by
  target, outcome, event, and time (CLI-254).
- `HOOK-168` `hooks list` shows, under each hook, its newest history record
  (matched on source, item, event, and command, where a record matches a
  declared command it ran in place of, its `replaced`): `last: <outcome>
  (<status>) <at>, ran in its place: <command>, log: <path>`, the
  `(<status>)` only for a hook that was started, `ran in its place` only for
  an override's record, and `log` only when one was saved. A hook with no
  record in the history shows nothing extra; the `ran at <commit>` status
  (HOOK-55) still says it last succeeded there.
- `HOOK-169` `hooks history --show` prints each shown record's saved log
  after its line, verbatim and sanitized (DSC-95) line by line, so each log
  line stays a line and a large log is streamed rather than read whole, or
  notes that the log is
  no longer on disk. Under `--json` each such record gains an `output`
  member: the log's text, or `null` when it is gone. A record with no log
  gets no `output`.
- `HOOK-170` `--hook-log-keep <all|failed>` (or the `hook-log-keep` config
  key, the flag outranking it) chooses which logs a logging run keeps. With
  `failed`, a hook that exits 0 has its log deleted as soon as it exits, and
  its record has no `log`; a hook that fails, cannot start, or is
  interrupted keeps its log. The flag also turns logging on, to the owned
  directory, for a run in which neither `--hook-log` nor the `hook-log` key
  did; the config key alone does not.
- `HOOK-171` A Ctrl-C while a hook runs reaches the hook and `mind` (the
  terminal signals the whole process group). `mind` does not die with it: it
  waits for the hook to exit, records it as `interrupted` with its exit code
  or signal, ends its log as usual, prints the closing frame line, and then
  stops with a `hook-interrupted` error (exit 130, CLI-255), whatever the
  hook's own exit, since the user asked `mind` to stop. A second Ctrl-C while
  the hook still runs (a hook that traps or ignores SIGINT) ends `mind` at
  once, killed by the signal, with no record written; the hook is left to
  exit on its own. When `mind`'s SIGINT is already ignored (a background
  job), it is left that way and the hook inherits it.
- `HOOK-172` A hook that fails, is aborted, or is interrupted stops the run
  even where `mind` otherwise skips a failing entry and carries on: a nested
  or marketplace source whose own install hook fails during `meld` or `sync`
  is not skipped as a clone failure (DSC-79), and a curator entry's profile
  member whose hook fails during `curate` is not skipped as an invalid
  selection (CUR-15). The run ends with the hook's error and its CLI-255 exit
  code.

## 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"`, `"update"` (HOOK-120), 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
  `--accept-hooks` runs every hook it accepts (HOOK-23, HOOK-150), optional and
  required alike. `optional` does NOT make the hook's failure tolerable (HOOK-53).
  An unrecognized reply is asked again and EOF runs the hook (CLI-257, CLI-258).
  A unified plan (HOOK-180) lists the hook as optional or required and answers
  it with the rest unless the user reviews each one (HOOK-181).
- `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
  `--accept-hooks`), `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 every unmeld path, the removal confirmation
  (CLI-260) 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) the hooks run before the
  source is removed, as on the default path. 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
  --accept-hooks` (or `--accept-hooks=uninstall`, CLI-250) 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). On a TTY the removal confirmation and the uninstall hook prompts
  are one plan (HOOK-186): the hooks are listed with the confirmation and
  answered there, and only a hook the user chose to review is prompted on its
  own.
- `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`.
  The record says a command ran, never that it is still wanted: `upgrade`
  re-offers a pending record only while the source's clone STILL declares that
  command as an install hook. A record whose declaration is gone (the source
  withdrew the hook, or its `mind.toml` no longer parses) is not re-offered,
  because the disclosure would then propose running a command no site declares,
  at a commit the author controls, under an `Event: install` heading. The two
  records that have no declaration to match are exempt, since the clone is not
  where they come from: a consumer override (HOOK-56) and a curated hook
  (HOOK-127).
- `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`.
  The override is recorded on the source and covers the install AND the update
  event: at every later `upgrade` it replaces the source's declared hooks for
  whichever of the two events is in effect (HOOK-122), with the same loud
  disclosure naming what it replaced. A source that moves its command from
  `event = "install"` to `event = "update"` therefore does not escape a consumer
  who replaced it, and the consumer's own command does not silently stop being
  re-run. There is no separate `--update-hook`: one override covers both.
- `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); `build` (like `bin`) is valid
only on a tool, and a schema error on any other kind (TOOL-7).

- `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). `build` is
  valid only on a `tool` item; declaring it on any other kind is a `mind.toml`
  schema error (TOOL-7).
- `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` `--accept-hooks` (bare, or a list naming `build`, HOOK-150) on
  `learn`, `upgrade`, `meld` (for its install-all path), `sync --upgrade`,
  `curate`, and `hooks run --event build` 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. 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 `--accept-hooks` 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`.

## Unified install prompt

Status: done. An interactive install used to ask several separate questions:
each source install hook (HOOK-20) before the item preview, then the install
confirmation (CLI-23, CLI-60), then each item's build and install hooks
(HOOK-72, HOOK-83) as each item installed. The user approved hooks before
seeing what would install, and a source hook had already run by the time the
user declined the install. These rules replace that sequence with one plan and
one answer, given before anything runs. `unmeld` has the same shape with its
removal confirmation (CLI-42) and uninstall hooks (HOOK-54, HOOK-82), and gets
the same treatment. HOOK-20, HOOK-24, HOOK-54, HOOK-142, CLI-21, CLI-23, and
CLI-60 point here.

- `HOOK-180` On a TTY, `meld` (its install-all offer, a `--learn` subset, a
  profile install, and a re-meld's pending hooks, HOOK-60), `learn`, and
  `upgrade` (including `sync --upgrade`) gather everything they would do into
  one plan, shown before any hook runs. The plan lists the items to install or
  upgrade (the CLI-23 preview, the CLI-61 report), then every hook the run would
  offer, in run order (HOOK-87): the source's pending install or update hooks
  (HOOK-55, HOOK-60, HOOK-122), then for each item its build hook (HOOK-70) and
  its install or update hooks (HOOK-81, HOOK-125). Each hook is shown with the
  fields of the HOOK-20 disclosure (name, event, required or optional, source
  identity, commit, command, working directory), sanitized per HOOK-91. A hook
  that `--accept-hooks` already accepts (HOOK-150) is listed and marked as
  accepted. The plan replaces the prompts the run would otherwise ask, so a run
  that would ask nothing today (a `learn` of items with no hooks) still asks
  nothing. `unmeld` follows the same pattern (HOOK-186). `hooks run`, `forget`,
  and `curate` are out of scope and keep their current prompts.
- `HOOK-181` The plan ends in one prompt, `[Y/s/r/n]`:
  - `y`/`Y`/Enter (the default): install the items and run every listed hook.
  - `s`: install the items and skip every listed hook. Each skip is reported and
    recorded as if it had been skipped at its own prompt (HOOK-21, HOOK-55,
    HOOK-110).
  - `r`: install the items and prompt each hook on its own at its run point, as
    today (HOOK-20, HOOK-52, HOOK-72, HOOK-83). This is the only way to answer
    hooks one at a time, and the only way to reach a required source hook's
    abort (HOOK-21).
  - `n`: install nothing and run no hook. On `meld` the source stays registered,
    as for a declined CLI-23 offer.

  An unrecognized reply is rejected and asked again (CLI-257), so an unclear
  answer neither runs nor skips anything. EOF takes the default, `y` (CLI-258):
  the user ran the command, so the intent is to run it fully. A removal is the
  exception: `unmeld`'s plan defaults to `n` (HOOK-186). A non-TTY run
  never reaches this prompt (HOOK-182).
- `HOOK-182` When the run has no items to install (`--register-only`, a re-meld
  with only pending hooks, or every item already installed) or `--yes` already
  answers the item part, the plan lists only the hooks and the prompt is
  `[Y/n/r]`: run every hook, skip every hook, or review each one. When
  `--accept-hooks` accepts every listed hook, the prompt is the existing item
  confirmation (CLI-23, CLI-60). When both flags cover the run, there is no
  prompt. A non-TTY run is unchanged (HOOK-22, CLI-23). `--json` output is
  unchanged.
- `HOOK-183` No hook runs before the plan is answered. On `meld`, the source's
  install hooks run after the answer and before the first item installs, which
  keeps the HOOK-87 order. On `upgrade`, the source hook pass (HOOK-11, HOOK-121)
  runs after the answer, so declining the upgrade runs no hook. Hooks still see
  the selection the run applies (HOOK-142). A hook `--accept-hooks` accepts
  needs no answer, so it is not held back: a meld runs an accepted source
  install hook at registration, as it did before the plan.
- `HOOK-184` An item hook's directory does not hold the item's files when the
  plan is shown: a build hook's staging directory (HOOK-71) does not exist yet,
  and an install or update hook's store directory (HOOK-81) is missing on a
  first install and holds the old version on an upgrade. So each item hook in
  the plan shows two locations: the directory it will run in, and the item's
  path in the source's clone, labeled as the place to inspect the code. The plan
  states that the clone copy is the item before token expansion (NS-11,
  TOOL-13). Source hooks are unaffected: they run in the clone, which is already
  at the disclosed commit (HOOK-24).
- `HOOK-185` An answer covers exactly the hooks the plan listed. A hook that
  comes up during the run without having been listed is prompted on its own
  (HOOK-20), whatever the answer was, so a plan that missed a hook never runs it
  without consent.
- `HOOK-186` `unmeld` uses the same plan and prompt. The plan lists the items it
  will remove (CLI-21; the matched sources for a glob, CLI-28), then every
  uninstall hook in teardown order (HOOK-87): each item's uninstall hooks
  (HOOK-82, HOOK-86), then the source's (HOOK-54). It replaces the CLI-42
  confirmation and the per-hook uninstall prompts, and no hook runs before the
  answer. The replies and their parsing are HOOK-181's, read for removal: `y`
  removes the items and runs every listed hook, `s` removes them and skips every
  hook, `r` removes them and prompts each hook on its own (a required source
  uninstall hook's abort still leaves the source melded, HOOK-54), and `n`
  removes nothing. Unlike an install, the default is `n` (`[y/s/r/N]`), so
  Enter and EOF remove nothing (CLI-260). Every `unmeld` asks it, including a
  single item and `--keep-items` (CLI-22), whose plan lists only the source's
  hooks. Under `--yes` the plan lists only the hooks and the prompt is
  HOOK-182's `[Y/n/r]`, whose default runs them: `--yes` already approved the
  removal. With `--accept-hooks` covering every hook, the prompt is the removal
  confirmation alone (`[y/N]`). A non-TTY `unmeld` without `--yes` refuses
  (CLI-260); with it, hooks are skipped as before (HOOK-54).

Limits of the plan, each covered by HOOK-185 (an unlisted hook is prompted on
its own):

- A nested source a curator registers during `meld` (DSC-54) keeps its own
  install hook prompt at registration, before the top-level plan.
- Aborting a fresh super-source meld at a reviewed source hook (`r`, then `a`)
  unregisters the top-level source but leaves the nested sources it already
  registered.
- `learn <source> --profile` asks its closure question (DEP-31) and then a
  hooks-only plan, and so do an `upgrade`'s profile and whole-source additions
  (PROF-21, LIFE-54) after the upgrade plan.
- On `upgrade` the source hook pass runs after the catalog scan, so an item a
  source hook generates is seen by the next `upgrade`.

## 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 `--accept-hooks` bypass
  (HOOK-23/74/150). 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-111` A source target honors the consumer's recorded `--install-hook`
  override (HOOK-56) as `upgrade` does: for `--event install` the override
  replaces every declared install hook (and is offered even when none is
  declared), and for `--event update` it replaces the declared update hooks
  when there are any. It runs as one required hook, under the same pending
  filter and `--force`, with the loud override note in its disclosure naming
  the declared commands it replaced, and its result record carries them as
  `replaced` (HOOK-160). So `hooks run` never runs a declared command the
  consumer replaced, with or without `--accept-hooks`. Uninstall hooks are
  unaffected (HOOK-59's override is given per `unmeld`, not recorded).
- `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>
  --accept-hooks=<event>`. `<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 (`--accept-hooks`) 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 --accept-hooks
  && 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
  `--accept-hooks`, 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. A prompt that reaches EOF takes its
  default (CLI-258), the same as a bare Enter at a real terminal, so the
  override still consents to nothing the user would not get by pressing Enter.
  The same test decides whether any fixed-choice prompt is shown (CLI-259).

## Update hooks

Status: done. An install hook runs at `meld`, and `upgrade` re-runs it when the
source advances (HOOK-11), so an install hook is executed many times over a
source's life. That is the right default -- tooling must track the source it was
built from -- but it makes idempotence a requirement rather than a courtesy, and
some setup steps cannot be made idempotent (a first-run migration, a one-shot
registration). The `update` event is the escape: a source (or an item) that needs
different behavior on a re-run declares an update hook, which runs *instead of*
the install hooks when the thing it belongs to is updated rather than first
installed.

- `HOOK-120` `event = "update"` is a third lifecycle event for a `[[hooks]]`
  entry (HOOK-51), valid on a source's `[[hooks]]` and on an item's hooks
  (`[[items.hooks]]`, the item manifest HOOK-131, the `update:` frontmatter
  scalar HOOK-130). It is otherwise an ordinary hook: same fields, same
  declaration-order execution, same disclosure, consent, and failure contract as
  an install hook (HOOK-20, HOOK-30, HOOK-52, HOOK-53, HOOK-83). The
  disclosure and every note name the event as `update`, so a user consenting to
  one is never told they are approving an install hook.
- `HOOK-121` A source's update hooks run at `upgrade` (including `sync
  --upgrade`), in the source's clone, when the source has a pending hook to run
  (HOOK-55's pending rule, applied to the update hooks). They do NOT run at
  `meld`, at a re-meld (HOOK-60), or at `unmeld`: the update event means "this
  source was already melded and has moved", which is exactly the state `meld`
  is not in.
  `meld` therefore records each declared update hook at the meld commit as a
  BASELINE: a record that never ran, so the hook is not pending until the source
  actually moves. Without the baseline the hook has no record at all, absent
  reads as pending (HOOK-55), and the first `upgrade` after a `meld` runs the
  update hook at the unmoved meld commit, on top of the install hook that just
  ran. A baseline never overwrites a real run, and `hooks list` reports it as
  "not pending (recorded at meld <commit>)" rather than as a run. It holds back
  only the automatic pass: an on-demand `mind hooks run <source> --event update`
  (HOOK-126) still considers the hook, since there the user named the target and
  the event and no run has happened.
- `HOOK-122` Update hooks supersede install hooks on an update, and only on an
  update. When a source declares at least one update hook, `upgrade` offers its
  pending update hooks and does NOT re-run the source's install hooks. When it
  declares none, `upgrade` re-offers the pending install hooks exactly as before
  (HOOK-11, HOOK-55). So the default behavior is unchanged for every existing
  source, and declaring an update hook is the opt-in to a distinct update path.
  Superseding is per scope: a source's update hooks replace the source's install
  hooks, and an item's update hooks replace that item's install hooks (HOOK-125);
  neither replaces the other's.
  A consumer `--install-hook` override outranks the supersession: when one is
  recorded (HOOK-56), it replaces whichever event's hooks are in effect, so the
  consumer's command is what is offered on either path.
- `HOOK-123` Because the default is a re-run, an install hook is expected to be
  idempotent: safe to run again against a host it already set up. `mind` does not
  and cannot enforce this -- a hook is an opaque shell command -- so it states it
  where an author meets the field: the `init-source` scaffold's commented
  `[[hooks]]` examples say so and show the `update` event as the alternative
  (HOOK-57), and the documentation says so. An author whose setup cannot be made
  idempotent declares an update hook instead of relying on the install hook's
  re-run.
- `HOOK-124` A source's update hooks are recorded in the same recorded-hook set
  as its install hooks (`install_hooks`, HOOK-55: an effective command plus the
  commit it last ran at, or null when skipped), keyed by the command AND the
  event. A pending update hook is therefore one whose recorded run-commit is
  absent or differs from the source's current commit, the same rule install
  hooks use, and `--force` re-offers every one regardless. An update hook that
  has already run at the source's current commit is not re-offered by a second
  `upgrade` at that commit.
  The event is half the key because the same command is a plausible declaration
  for both events ("same script, different trigger"): keyed by command alone, an
  install run would settle the update hook, `hooks run <src> --event update`
  would exit 0 having run nothing, and a run recorded under one event could be
  re-offered under the other's disclosure. A record written before the event was
  stored carries none; it reads as an install record, since that is the only
  event that existed when it was written.
- `HOOK-125` An item's update hooks run in place of its install hooks whenever
  the item is re-installed over an existing install of the same effective name,
  with the same working directory, ordering, recording, and rollback contract as
  the install hooks they replace (HOOK-81, HOOK-110). On a first install the
  install hooks run and the update hooks do not. In practice the re-install path
  is `upgrade`'s in-place swap: a `learn` naming an already-installed item is a
  no-op that installs nothing and so runs no hook at all (CLI-157, DEP-23), and
  an `upgrade` that RENAMES an item (a namespace change) is not a re-install
  either -- the old name is removed with its uninstall hooks and the new name is
  a first install. `hooks run --event update` (HOOK-126) runs an item's update
  hooks on demand without reinstalling it. An item that declares no update hook re-runs
  its install hooks on every (re)install, unchanged (HOOK-84). An item's
  `build` hook is unaffected: it is content-producing and always runs (HOOK-73).
- `HOOK-126` `--event update` is valid for `hooks run` (CLI-195) on both a source
  and an item target, and behaves as `--event install` does at that target: it
  runs the target's pending update hooks (all of them under `--force`), records
  each run exactly as an install run does (HOOK-101, HOOK-110), and participates
  in the HOOK-107/HOOK-108 tally and the `HooksNotRun` report. `hooks list` takes
  no `--event` filter (CLI-196): it reports every event, so a declared update
  hook appears alongside the install and uninstall ones with its event,
  required/optional flag, command, and pending state. `mind review` lists a
  declared update hook the same way (HOOK-58, HOOK-85).
  When `hooks run` finds hooks for the selected event but the pending filter
  holds every one of them back, it says so: a note naming the hooks, the commit
  they already ran at, and the `--force` invocation that runs them anyway. A
  silent exit 0 is indistinguishable from "this source declares no hooks",
  which is the opposite diagnosis.
- `HOOK-127` A curator's `[[discover.sources.hooks]]` entries (DSC-61) are
  recorded on the nested source at `meld`, marked as curated, and are offered by
  `upgrade` from that record. They are declared in the PARENT super-source's
  manifest, so the nested clone can never be asked what it declares (HOOK-55),
  and they apply exactly when the nested source ships no `mind.toml` of its own
  (the DSC-60 gate). Without the record a curated `event = "update"` entry could
  never run at all (it is filtered out at `meld` by HOOK-121 and is invisible in
  the clone), and curated install hooks would be re-offered at every `upgrade`
  in defiance of HOOK-122's superseding rule. A curated record stops applying
  once the nested source gains a `mind.toml`, which is the same gate evaluated
  at `meld`. The record carries the entry's `name` and `optional` flag, since
  there is no declaration to read them back from.

## Where an item declares its hooks

Status: done. An item's lifecycle hooks (HOOK-80/86) could originally be declared
only in the source's root `mind.toml`, except for a tool, which could also carry
scalar `install:`/`uninstall:` keys in its `TOOL.md` frontmatter. That put the
declaration far from the thing it belongs to for every other kind: a skill that
needs a setup command had to make its source's root manifest authoritative
(turning off convention discovery for the whole repo) to say so. An item declares
its own hooks instead, next to the item.

- `HOOK-130` The scalar lifecycle keys `install:`, `update:`, and `uninstall:`
  are read from ANY kind's meta file frontmatter -- a skill's `SKILL.md`, a
  tool's `TOOL.md`, and an agent's or rule's own `.md` -- not only a tool's
  `TOOL.md`. This supersedes HOOK-80's tool-only restriction on the frontmatter
  spelling (the `mind.toml` spelling was already valid on any kind). Each is one
  required hook of its event (HOOK-80), and the frontmatter reader stays
  scalar-only (DSC-21), so the array form still requires a manifest. An empty or
  whitespace-only value is absent (HOOK-3).
  Compatibility: frontmatter is free-form, and `mind` previously ignored these
  keys on every kind but a tool. A repo whose `SKILL.md` carries `install: pip
  install foo` as documentation therefore now ships a hook to every consumer,
  who is prompted to run a command the author never meant as one. An author in
  that position renames the key (there is no reserved-prefix escape: the three
  names are the hook spelling). A consumer sees it before installing anything,
  because `mind review` reports every declared item hook (HOOK-85, HOOK-134).
- `HOOK-131` A directory-backed item (a skill or a tool) may ship its own
  `mind.toml` in its item directory: a scoped-down manifest whose only table is
  `[[hooks]]`, with exactly the fields and semantics of a source's `[[hooks]]`
  (HOOK-51 plus the `update` event, HOOK-120). Any other key is a `mind.toml`
  schema error naming the file, so a root manifest copied into an item directory
  fails loudly instead of being half-read. On a catalog scan the failure is
  scoped to the affected item: that one item is dropped with a warning naming
  the file, so one stray manifest cannot take down the source or the registry
  walk (see [discovery.md]discovery.md). An item manifest is read only from an
  item's own directory and never from the source root, and the source's root
  `mind.toml` is never read as an item manifest, even for a source root that is
  also an item path.
- `HOOK-132` The three declaration sites do not merge; exactly one supplies an
  item's hooks, in this precedence: (1) a root `mind.toml` `[[items]]`
  declaration for that item that declares any hook (a scalar `install`/`update`/
  `uninstall` or an `[[items.hooks]]` entry) is authoritative and its hooks are
  the item's hooks; else (2) the item directory's own `mind.toml` `[[hooks]]`
  (HOOK-131), when it declares any; else (3) the item's frontmatter scalars
  (HOOK-130). Replacement rather than composition keeps an item's effective hook
  list traceable to one origin, so a consumer reading `mind review` or `hooks
  list` sees a set that exists somewhere as written.
  Replacement is whole-site, which is the trap: a site that declares one hook
  takes over the item's whole hook list, and the hooks the losing sites declared
  are silently gone. A `[[items]]` entry with only `install = "./setup.sh"`
  drops the item's own `uninstall:` frontmatter, so its teardown never runs; an
  item-directory `mind.toml` (HOOK-131) declaring only an update hook drops the
  frontmatter `install:`, so a first install runs nothing. An author who wants
  both writes both at the SAME site.
- `HOOK-133` An item manifest is part of the item's content: it is copied into
  the store with the rest of the item and participates in the content hash, so
  editing it upstream is drift like any other file change and `upgrade` picks it
  up. It is not linked or interpreted by any agent harness; only `mind` reads it.
- `HOOK-134` `mind review` (HOOK-85) and `hooks list` (HOOK-104) report an
  item's hooks from its RESOLVED hook list, whichever of the three sites
  (HOOK-132) supplied it, rather than from the root manifest's scalar fields
  alone. So an `[[items.hooks]]` array entry, an item-manifest hook, and a
  frontmatter scalar are all disclosed, each naming its event.

## Hook environment

Status: done. A source with install profiles (spec/profiles.md) may want its
hooks to do different setup depending on which profiles the consumer enabled.
`mind` tells each hook through its environment.

- `HOOK-140` Every hook `mind` runs gets two environment variables: a source's
  install and uninstall hooks, an item's install, update, and uninstall hooks, a
  tool build hook, and each of these when run through `hooks run`.
  - `MIND_PROFILES`: the enabled profiles of the hook's source.
  - `MIND_ITEM_PROFILES`: for an item hook or a build hook, the profiles the
    item is installed for (PROF-20). Always empty for a source hook.

  Each value is a sorted, comma-separated list with no spaces. Profile names
  follow PROF-2's syntax, so the values need no quoting. Both variables are
  always set, empty when there is nothing to report, so a value inherited from
  the environment `mind` itself runs in never reaches the hook.
- `HOOK-141` The values describe the profile state the operation leaves behind:
  - While a profile install is applied (`meld`, a re-meld, `learn <source>
    --profile`, `curate`, an `upgrade` addition per PROF-21), `MIND_PROFILES`
    is the recorded set plus the profiles being enabled, or the new set alone
    when the install replaces the recorded one (PROF-40, PROF-41). An installed item's
    `MIND_ITEM_PROFILES` includes the profiles it is being installed for. For a
    closure item, those are the profiles of the member that pulled it in
    (PROF-6).
  - During `forget <source> --profile <NAME>`, `MIND_PROFILES` is the set that
    stays enabled. A removed item's `MIND_ITEM_PROFILES` is its recorded
    reasons, NAME included.
  - Otherwise (a plain `learn`, `upgrade`, `forget`, `unmeld`, `hooks run`),
    they are the source's recorded set and the item's recorded reasons. A source
    that is not profile-tracked (PROF-19) has an empty `MIND_PROFILES`.
- `HOOK-142` A source's install hooks at `meld` see the selection the meld is
  about to apply: the profile flags plus, on an interactive meld, the PROF-13
  picker's answer. The picker is therefore asked before the hooks run. A re-meld's
  re-offered hooks see the re-meld's selection (PROF-16): added to the
  recorded set, or alone when the flags replace it (PROF-40). The value is the
  selection, not the outcome. On an interactive meld the hooks run after the
  unified plan is answered (HOOK-183), and declining it (`n`) runs none of them.
  Elsewhere (`--accept-hooks`, a nested curated source) they run before the
  install prompt, so a hook still sees a selection whose install is later
  declined. A name the source does
  not declare (PROF-14) is left out. `MIND_PROFILES` is empty when no profile
  install follows: `--register-only`, the CLI-23 install-all offer, and a nested
  source whose curator entry routes outside profiles (PROF-26).
- `HOOK-143` Enabling or disabling a profile later does not re-offer a source's
  install hooks; HOOK-60 is unchanged. A setup step that depends on the profile
  set belongs in an item hook, which runs when the item it serves is installed or
  removed.
- `HOOK-144` Every hook also runs with `MIND_IN_HOOK=1`. The `mind` command
  that runs a hook holds mind's lock (STO-40) until the hook exits, so a `mind`
  the hook starts would wait for that lock forever. A `mind` command that runs
  with `MIND_IN_HOOK` set and finds the lock held fails at once with a
  `nested-in-hook` error instead of waiting, which fails the hook. With the
  lock free (a process the hook left running after `mind` exited), it runs
  normally.

## Items hooks

Status: done. A source's install and uninstall hooks run at `meld`
and `unmeld`, and an item's hooks run once per item. Neither fits work that depends
on the set of a source's installed items: a registry, index, or settings
fragment generated from the installed tools, or a resource several items share
that should exist while any of them is installed. An items hook is a
source-level hook that runs once per command with the source's installed set,
so the author writes "make the host match this set" instead of tracking each
install and removal. It is another layer, not a replacement: item hooks and
source hooks run as before.

- `HOOK-190` `event = "items"` is a fourth lifecycle event for a source's
  `[[hooks]]` entry (HOOK-51) and a curator's `[[discover.sources.hooks]]`
  entry (DSC-61, HOOK-127). It has the same fields, declaration-order execution,
  and `optional` semantics as the other events (HOOK-52). It is not valid on an
  item's hooks (`[[items.hooks]]`, HOOK-131's item manifest, HOOK-130's
  frontmatter); there it is a `mind.toml` schema error naming the bad value and
  the events legal for an item.
- `HOOK-191` An items hook is pending when the source's installed set differs
  from the set the hook last applied (HOOK-195). A set differs when an item was
  added, an item was removed, or an installed item's content hash changed. At
  the end of any command that installs, updates, or removes items of the source,
  each pending items hook of that source is offered once:
  - installs: `meld`, `learn` (including `learn <source> --profile`), `curate`,
    `absorb`, an `upgrade` addition (LIFE-54, PROF-21), and an `auto_meld`
    install (POL-58);
  - updates: `upgrade`'s in-place swap (an `upgrade` rename is a removal plus
    an install);
  - removals: `forget` (including `forget <source> --profile`), `prune`, an
    `upgrade` prune pass (PROF-39), and `unmeld`.

  `upgrade` also offers a pending items hook when it changed nothing, so a run
  that was skipped or failed earlier catches up. A command that touches several
  sources offers each source's items hooks once, after that source's items.
  `unmeld --keep-items` (CLI-22) removes no items and does not offer one. A
  `probe` action (TUI-20..23) offers them as the verb it runs does; one that
  runs with its output captured (TUI-24) and reaches an items hook its plan did
  not answer skips it, and the hook stays pending.
- `HOOK-192` An items hook runs in the source's clone at the source's current
  commit, after every item install, update, and removal of the command and after
  those items' own hooks (HOOK-81, HOOK-82, HOOK-125). This extends HOOK-87:
  `source.install -> item.install* -> items ... item.uninstall* -> items ->
  source.uninstall`. At `unmeld` the items hook runs after the items are
  removed, with an empty installed set, and before the source's uninstall hooks
  and the removal of the clone.
- `HOOK-193` An items hook gets `MIND_ITEMS_FILE`, the path to a JSON document
  that `mind` writes before the hook runs and deletes after it exits:

  ```json
  {
    "version": 1,
    "source": "github.com/acme/tools",
    "commit": "<sha>",
    "command": "learn",
    "added": [],
    "updated": [],
    "removed": [],
    "installed": []
  }
  ```

  `command` is the verb that offered the hook (`hooks run` for HOOK-196).
  `installed` is the source's full installed set after the command. `added`,
  `updated`, and `removed` are the difference between that set and the set the
  hook last applied (HOOK-195), not the command's own changes, so changes from
  a skipped or failed run are not lost. On the first run every installed item is
  in `added`. Each entry has `kind`, `name` (the bare name), `effective_name`,
  `store` (the absolute store path; for a removed item, the path it had, which
  no longer exists), and `profiles` (the item's recorded reasons, PROF-20).
  Each list is sorted by kind, then effective name. A file is used instead of
  environment variables because a large source can exceed the per-string
  environment limit.

  The hook also gets HOOK-140's variables (`MIND_ITEM_PROFILES` empty) and
  `MIND_IN_HOOK` (HOOK-144). A nested `mind` cannot run while the hook holds the
  lock, so the document carries everything the hook can learn about the
  installed set. Every other hook gets `MIND_ITEMS_FILE` set to empty, for the
  same reason HOOK-140's variables are always set. Cleanup that needs a removed
  item's files belongs in that item's uninstall hook, which runs while they
  still exist.
- `HOOK-194` An items hook is disclosed, prompted, and skipped like a source
  hook (HOOK-20, HOOK-22, HOOK-52, HOOK-91). A plan lists it (HOOK-198); one
  no plan listed is prompted on its own (HOOK-185). A required items hook's abort
  is a skip: the command's items are already applied. A skip without a
  terminal prints the `hooks run` remedy (HOOK-106). `--accept-hooks` accepts
  it bare or as `--accept-hooks=items`, which extends CLI-250's event list.
  Its results go into the `--json` document, the error envelope, and the hook
  history with event `items` (HOOK-160, HOOK-163).
- `HOOK-195` The source's registry entry records, for each items hook (keyed
  by its command), the installed set it last applied: each item's entry
  (HOOK-193) and content hash. A successful run
  replaces the record with the set in the `installed` list it was given. A skip,
  a decline, or a failure leaves the record unchanged, so the hook stays
  pending. A non-zero exit does not roll back the command's item changes,
  because they and their own hooks have already run. The command reports the
  failure, saves its state (LIFE-48), and exits 3 (CLI-255). The record is
  dropped with the source at `unmeld`. A source that newly declares an items
  hook has no record, so the hook's first offer lists every installed item as
  added.
- `HOOK-196` `hooks run <source> --event items` (CLI-195) runs the source's
  pending items hooks with `command` set to `hooks run`. `--force` runs them
  even when they are not pending, in which case the three difference lists may
  be empty. An item target never runs an items hook. `hooks list` reports each
  items hook with its pending state and the size of its recorded set. `review`
  (HOOK-58) and `recall --sources` disclose it like any other source hook.
- `HOOK-197` `init-source` scaffolds a commented `event = "items"` example
  alongside its other `[[hooks]]` examples (HOOK-57). Its comment states that
  the hook receives the full installed set in `MIND_ITEMS_FILE` and should make
  the host match it, so a re-run with the same set changes nothing.
- `HOOK-198` The unified plan (HOOK-180, HOOK-186) lists each items
  hook the run will offer, in run order (HOOK-192), and its answer covers it
  (HOOK-181). An install plan lists the items hooks of the sources it installs
  into; an `upgrade` plan also lists the pending ones in scope; an `unmeld` plan
  lists the source's, between the items' uninstall hooks and the source's. A
  listed hook is offered at the end of the command under the answer even when
  the command left its source's set unchanged, and runs if it is still pending
  then (HOOK-191). A source the managed policy locks out (POL-12) runs no hook,
  so the plan does not list its items hooks.

## 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
  `--accept-hooks` (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.