clawvault 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/README.md +54 -14
  2. package/bin/clawvault.js +0 -2
  3. package/bin/command-registration.test.js +13 -1
  4. package/bin/help-contract.test.js +14 -0
  5. package/bin/register-core-commands.js +88 -0
  6. package/bin/register-core-commands.test.js +80 -0
  7. package/bin/register-maintenance-commands.js +57 -6
  8. package/bin/register-query-commands.js +10 -28
  9. package/bin/test-helpers/cli-command-fixtures.js +1 -0
  10. package/dist/chunk-2PKBIKDH.js +130 -0
  11. package/dist/{chunk-2JQ3O2YL.js → chunk-5EFSWZO6.js} +3 -3
  12. package/dist/{chunk-77Q5CSPJ.js → chunk-7SWP5FKU.js} +33 -701
  13. package/dist/{chunk-URXDAUVH.js → chunk-AXSJIFOJ.js} +174 -1
  14. package/dist/{chunk-23YDQ3QU.js → chunk-BLQXXX7Q.js} +6 -6
  15. package/dist/chunk-CSHO3PJB.js +684 -0
  16. package/dist/{chunk-SLXOR3CC.js → chunk-DOIUYIXV.js} +2 -2
  17. package/dist/{chunk-NCKFNBHJ.js → chunk-DVOUSOR3.js} +79 -5
  18. package/dist/{chunk-CLJTREDS.js → chunk-ECGJYWNA.js} +193 -41
  19. package/dist/{chunk-BUEW6IIK.js → chunk-EL6UBSX5.js} +5 -5
  20. package/dist/{chunk-6FH3IULF.js → chunk-FZ5I2NF7.js} +1 -1
  21. package/dist/{chunk-ZN54U2OZ.js → chunk-GFCHWMGD.js} +3 -3
  22. package/dist/{chunk-GNJL4YGR.js → chunk-GJO3CFUN.js} +30 -6
  23. package/dist/chunk-H3JZIB5O.js +322 -0
  24. package/dist/chunk-HEHO7SMV.js +51 -0
  25. package/dist/{chunk-STCQGCEQ.js → chunk-HGDDW24U.js} +3 -3
  26. package/dist/chunk-J3YUXVID.js +907 -0
  27. package/dist/{chunk-Y6VJKXGL.js → chunk-KCYWJDDW.js} +1 -1
  28. package/dist/{chunk-W4SPAEE7.js → chunk-OFOCU2V4.js} +5 -4
  29. package/dist/chunk-PTWPPVC7.js +972 -0
  30. package/dist/{chunk-QSHD36LH.js → chunk-QFWERBDP.js} +2 -2
  31. package/dist/{chunk-QSRRMEYM.js → chunk-S7N7HI5E.js} +1 -1
  32. package/dist/{chunk-PBACDKKP.js → chunk-T7E764W3.js} +3 -3
  33. package/dist/chunk-TDWFBDAQ.js +1016 -0
  34. package/dist/{chunk-ESVS6K2B.js → chunk-TWMI3SNN.js} +6 -5
  35. package/dist/{chunk-2RAZ4ZFE.js → chunk-VBILES4B.js} +1 -1
  36. package/dist/{chunk-ESFLMDRB.js → chunk-VXAGOLDP.js} +3 -3
  37. package/dist/chunk-YCUVAOFC.js +158 -0
  38. package/dist/{chunk-SS4B7P7V.js → chunk-YIDV4VV2.js} +1 -1
  39. package/dist/chunk-ZKWPCBYT.js +600 -0
  40. package/dist/cli/index.js +24 -24
  41. package/dist/commands/archive.js +2 -2
  42. package/dist/commands/benchmark.d.ts +12 -0
  43. package/dist/commands/benchmark.js +12 -0
  44. package/dist/commands/context.js +6 -5
  45. package/dist/commands/doctor.d.ts +8 -3
  46. package/dist/commands/doctor.js +6 -20
  47. package/dist/commands/embed.js +5 -4
  48. package/dist/commands/entities.js +1 -1
  49. package/dist/commands/graph.js +2 -2
  50. package/dist/commands/inbox.d.ts +23 -0
  51. package/dist/commands/inbox.js +11 -0
  52. package/dist/commands/inject.d.ts +1 -1
  53. package/dist/commands/inject.js +3 -3
  54. package/dist/commands/link.js +6 -6
  55. package/dist/commands/maintain.d.ts +32 -0
  56. package/dist/commands/maintain.js +12 -0
  57. package/dist/commands/migrate-observations.js +2 -2
  58. package/dist/commands/observe.js +9 -8
  59. package/dist/commands/rebuild-embeddings.js +47 -16
  60. package/dist/commands/rebuild.js +7 -6
  61. package/dist/commands/reflect.js +5 -5
  62. package/dist/commands/replay.js +8 -7
  63. package/dist/commands/setup.js +3 -2
  64. package/dist/commands/sleep.d.ts +1 -1
  65. package/dist/commands/sleep.js +17 -15
  66. package/dist/commands/status.js +26 -24
  67. package/dist/commands/sync-bd.js +2 -2
  68. package/dist/commands/tailscale.js +2 -2
  69. package/dist/commands/wake.d.ts +1 -1
  70. package/dist/commands/wake.js +8 -7
  71. package/dist/index.d.ts +168 -16
  72. package/dist/index.js +271 -108
  73. package/dist/{inject-DYUrDqQO.d.ts → inject-DEb_jpLi.d.ts} +3 -1
  74. package/dist/lib/config.js +1 -1
  75. package/dist/{types-BbWJoC1c.d.ts → types-DslKvCaj.d.ts} +51 -1
  76. package/hooks/clawvault/HOOK.md +22 -5
  77. package/hooks/clawvault/handler.js +213 -78
  78. package/hooks/clawvault/handler.test.js +109 -43
  79. package/hooks/clawvault/integrity.js +112 -0
  80. package/hooks/clawvault/integrity.test.js +32 -0
  81. package/hooks/clawvault/openclaw.plugin.json +133 -15
  82. package/openclaw.plugin.json +126 -20
  83. package/package.json +2 -2
  84. package/bin/register-workgraph-commands.js +0 -1368
  85. package/dist/chunk-33VSQP4J.js +0 -37
  86. package/dist/chunk-4BQTQMJP.js +0 -93
  87. package/dist/chunk-EK6S23ZB.js +0 -469
  88. package/dist/chunk-GAOWA7GR.js +0 -501
  89. package/dist/chunk-GGA32J2R.js +0 -784
  90. package/dist/chunk-MM6QGW3P.js +0 -207
  91. package/dist/chunk-QVEERJSP.js +0 -152
  92. package/dist/chunk-U4O6C46S.js +0 -154
  93. package/dist/chunk-VSL7KY3M.js +0 -189
  94. package/dist/chunk-WMGIIABP.js +0 -15
  95. package/dist/commands/workgraph.d.ts +0 -124
  96. package/dist/commands/workgraph.js +0 -38
  97. package/dist/ledger-B7g7jhqG.d.ts +0 -44
  98. package/dist/registry-BR4326o0.d.ts +0 -30
  99. package/dist/store-CA-6sKCJ.d.ts +0 -34
  100. package/dist/thread-B9LhXNU0.d.ts +0 -41
  101. package/dist/workgraph/index.d.ts +0 -5
  102. package/dist/workgraph/index.js +0 -23
  103. package/dist/workgraph/ledger.d.ts +0 -2
  104. package/dist/workgraph/ledger.js +0 -25
  105. package/dist/workgraph/registry.d.ts +0 -2
  106. package/dist/workgraph/registry.js +0 -19
  107. package/dist/workgraph/store.d.ts +0 -2
  108. package/dist/workgraph/store.js +0 -25
  109. package/dist/workgraph/thread.d.ts +0 -2
  110. package/dist/workgraph/thread.js +0 -25
  111. package/dist/workgraph/types.d.ts +0 -54
  112. package/dist/workgraph/types.js +0 -7
@@ -87,20 +87,35 @@ openclaw config set plugins.entries.clawvault.config.vaultPath ~/my-vault
87
87
  openclaw config get plugins.entries.clawvault
88
88
  ```
89
89
 
90
- Available configuration options:
90
+ Available configuration options (all privileged actions are opt-in):
91
91
 
92
92
  | Key | Type | Default | Description |
93
93
  |-----|------|---------|-------------|
94
94
  | `vaultPath` | string | (auto-detected) | Path to the ClawVault vault directory |
95
- | `autoCheckpoint` | boolean | `true` | Enable automatic checkpointing on session events |
95
+ | `agentVaults` | object | `{}` | Per-agent vault mapping |
96
+ | `allowClawvaultExec` | boolean | `false` | Required gate for all `child_process` calls |
97
+ | `clawvaultBinaryPath` | string | (PATH lookup) | Optional absolute path to `clawvault` binary |
98
+ | `clawvaultBinarySha256` | string | (unset) | Optional SHA-256 executable integrity check |
99
+ | `allowEnvAccess` | boolean | `false` | Allow env fallbacks (`OPENCLAW_*`, `CLAWVAULT_PATH`) |
100
+ | `enableStartupRecovery` | boolean | `false` | Enable `gateway:startup` recovery check |
101
+ | `enableSessionContextInjection` | boolean | `false` | Enable `session:start` recap/context injection |
102
+ | `enableAutoCheckpoint` | boolean | `false` | Enable checkpoint on `command:new` |
103
+ | `enableObserveOnNew` | boolean | `false` | Enable observer flush on `command:new` |
104
+ | `enableHeartbeatObservation` | boolean | `false` | Enable heartbeat-driven observation |
105
+ | `enableCompactionObservation` | boolean | `false` | Enable observer flush on compaction |
106
+ | `enableWeeklyReflection` | boolean | `false` | Enable weekly reflection cron |
107
+ | `enableFactExtraction` | boolean | `false` | Enable local fact extraction/entity graph updates |
108
+ | `autoCheckpoint` | boolean | `false` | Deprecated alias for `enableAutoCheckpoint` |
96
109
  | `contextProfile` | string | `"auto"` | Default context profile (`default`, `planning`, `incident`, `handoff`, `auto`) |
97
110
  | `maxContextResults` | integer | `4` | Maximum context results to inject on session start |
98
- | `observeOnHeartbeat` | boolean | `true` | Enable observation threshold checks on heartbeat |
99
- | `weeklyReflection` | boolean | `true` | Enable weekly reflection on Sunday midnight UTC |
111
+ | `observeOnHeartbeat` | boolean | `false` | Deprecated alias for `enableHeartbeatObservation` |
112
+ | `weeklyReflection` | boolean | `false` | Deprecated alias for `enableWeeklyReflection` |
113
+
114
+ Security details and threat model: see [SECURITY.md](../../SECURITY.md).
100
115
 
101
116
  ### Vault Path Resolution
102
117
 
103
- The hook resolves the vault path in this order:
118
+ When `allowEnvAccess=true`, the hook resolves the vault path in this order:
104
119
 
105
120
  1. Plugin config (`plugins.entries.clawvault.config.vaultPath` set via `openclaw config`)
106
121
  2. `OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH` environment variable
@@ -108,6 +123,8 @@ The hook resolves the vault path in this order:
108
123
  4. Walking up from cwd to find `.clawvault.json`
109
124
  5. Checking `memory/` subdirectory (OpenClaw convention)
110
125
 
126
+ When `allowEnvAccess=false` (default), steps 2 and 3 are skipped.
127
+
111
128
  ### Troubleshooting
112
129
 
113
130
  If `openclaw hooks enable clawvault` fails with hook-not-found, run `openclaw hooks install clawvault` first and verify discovery with `openclaw hooks list --verbose`.
@@ -16,6 +16,11 @@ import { createHash, randomUUID } from 'crypto';
16
16
  import * as fs from 'fs';
17
17
  import * as os from 'os';
18
18
  import * as path from 'path';
19
+ import {
20
+ resolveExecutablePath,
21
+ sanitizeExecArgs,
22
+ verifyExecutableIntegrity
23
+ } from './integrity.js';
19
24
 
20
25
  const MAX_CONTEXT_RESULTS = 4;
21
26
  const MAX_CONTEXT_PROMPT_LENGTH = 500;
@@ -36,6 +41,7 @@ const MAX_FACT_TEXT_LENGTH = 600;
36
41
  const FACT_SENTENCE_SPLIT_RE = /[.!?]+\s+|\r?\n+/;
37
42
  const EXCLUSIVE_FACT_RELATIONS = new Set(['lives_in', 'works_at', 'age']);
38
43
  const ENTITY_TARGET_RELATIONS = new Set(['works_at', 'lives_in', 'partner_name', 'dog_name', 'parent_name']);
44
+ const CLAWVAULT_EXECUTABLE = 'clawvault';
39
45
 
40
46
  // Sanitize string for safe display (prevent prompt injection via control chars)
41
47
  function sanitizeForDisplay(str) {
@@ -107,15 +113,17 @@ function normalizeAbsoluteEnvPath(value) {
107
113
  return resolved;
108
114
  }
109
115
 
110
- function getOpenClawAgentsDir() {
111
- const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
112
- if (stateDir) {
113
- return path.join(stateDir, 'agents');
114
- }
116
+ function getOpenClawAgentsDir(pluginConfig) {
117
+ if (allowsEnvAccess(pluginConfig)) {
118
+ const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
119
+ if (stateDir) {
120
+ return path.join(stateDir, 'agents');
121
+ }
115
122
 
116
- const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
117
- if (openClawHome) {
118
- return path.join(openClawHome, 'agents');
123
+ const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
124
+ if (openClawHome) {
125
+ return path.join(openClawHome, 'agents');
126
+ }
119
127
  }
120
128
 
121
129
  return path.join(os.homedir(), '.openclaw', 'agents');
@@ -155,8 +163,8 @@ function getScaledObservationThresholdBytes(fileSizeBytes) {
155
163
  return LARGE_SESSION_THRESHOLD_BYTES;
156
164
  }
157
165
 
158
- function parseSessionIndex(agentId) {
159
- const sessionsDir = path.join(getOpenClawAgentsDir(), agentId, 'sessions');
166
+ function parseSessionIndex(agentId, pluginConfig) {
167
+ const sessionsDir = path.join(getOpenClawAgentsDir(pluginConfig), agentId, 'sessions');
160
168
  const sessionsJsonPath = path.join(sessionsDir, 'sessions.json');
161
169
  if (!fs.existsSync(sessionsJsonPath)) {
162
170
  return { sessionsDir, index: {} };
@@ -173,9 +181,9 @@ function parseSessionIndex(agentId) {
173
181
  }
174
182
  }
175
183
 
176
- function shouldObserveActiveSessions(vaultPath, agentId) {
184
+ function shouldObserveActiveSessions(vaultPath, agentId, pluginConfig) {
177
185
  const cursors = loadObserveCursors(vaultPath);
178
- const { sessionsDir, index } = parseSessionIndex(agentId);
186
+ const { sessionsDir, index } = parseSessionIndex(agentId, pluginConfig);
179
187
  const entries = Object.entries(index);
180
188
  if (entries.length === 0) {
181
189
  return false;
@@ -472,6 +480,31 @@ function extractPluginConfig(event) {
472
480
  return {};
473
481
  }
474
482
 
483
+ function isOptInEnabled(pluginConfig, ...keys) {
484
+ for (const key of keys) {
485
+ if (pluginConfig?.[key] === true) return true;
486
+ }
487
+ return false;
488
+ }
489
+
490
+ function allowsEnvAccess(pluginConfig) {
491
+ return isOptInEnabled(pluginConfig, 'allowEnvAccess');
492
+ }
493
+
494
+ function getConfiguredExecutablePath(pluginConfig) {
495
+ const value = pluginConfig?.clawvaultBinaryPath;
496
+ if (typeof value !== 'string') return null;
497
+ const trimmed = value.trim();
498
+ return trimmed || null;
499
+ }
500
+
501
+ function getConfiguredExecutableSha256(pluginConfig) {
502
+ const value = pluginConfig?.clawvaultBinarySha256;
503
+ if (typeof value !== 'string') return null;
504
+ const trimmed = value.trim().toLowerCase();
505
+ return trimmed || null;
506
+ }
507
+
475
508
  // Resolve vault path for a specific agent from agentVaults config
476
509
  function resolveAgentVaultPath(pluginConfig, agentId) {
477
510
  if (!agentId || typeof agentId !== 'string') return null;
@@ -489,11 +522,9 @@ function resolveAgentVaultPath(pluginConfig, agentId) {
489
522
 
490
523
  // Find vault by walking up directories
491
524
  // Supports per-agent vault paths via agentVaults config
492
- function findVaultPath(event, options = {}) {
493
- const pluginConfig = extractPluginConfig(event);
494
-
525
+ function findVaultPath(event, pluginConfig, options = {}) {
495
526
  // Determine agent ID for per-agent vault resolution
496
- const agentId = options.agentId || resolveAgentIdForEvent(event);
527
+ const agentId = options.agentId || resolveAgentIdForEvent(event, pluginConfig);
497
528
 
498
529
  // Check agentVaults first (per-agent vault paths)
499
530
  if (agentId) {
@@ -510,15 +541,17 @@ function findVaultPath(event, options = {}) {
510
541
  if (validated) return validated;
511
542
  }
512
543
 
513
- // Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
514
- if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
515
- const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
516
- if (validated) return validated;
517
- }
544
+ if (allowsEnvAccess(pluginConfig)) {
545
+ // Check OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH env (injected by OpenClaw from plugin config)
546
+ if (process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH) {
547
+ const validated = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH);
548
+ if (validated) return validated;
549
+ }
518
550
 
519
- // Check CLAWVAULT_PATH env
520
- if (process.env.CLAWVAULT_PATH) {
521
- return validateVaultPath(process.env.CLAWVAULT_PATH);
551
+ // Check CLAWVAULT_PATH env
552
+ if (process.env.CLAWVAULT_PATH) {
553
+ return validateVaultPath(process.env.CLAWVAULT_PATH);
554
+ }
522
555
  }
523
556
 
524
557
  // Walk up from cwd
@@ -541,16 +574,54 @@ function findVaultPath(event, options = {}) {
541
574
  }
542
575
 
543
576
  // Run clawvault command safely (no shell)
544
- function runClawvault(args, options = {}) {
577
+ function runClawvault(args, pluginConfig, options = {}) {
578
+ if (!isOptInEnabled(pluginConfig, 'allowClawvaultExec')) {
579
+ return {
580
+ success: false,
581
+ skipped: true,
582
+ output: 'ClawVault CLI execution is disabled. Set allowClawvaultExec=true to enable.',
583
+ code: 0
584
+ };
585
+ }
586
+
545
587
  const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1000, Number(options.timeoutMs)) : 15000;
588
+ const executablePath = resolveExecutablePath(CLAWVAULT_EXECUTABLE, {
589
+ explicitPath: getConfiguredExecutablePath(pluginConfig)
590
+ });
591
+ if (!executablePath) {
592
+ return {
593
+ success: false,
594
+ output: 'Unable to resolve clawvault executable path. Set clawvaultBinaryPath to an absolute executable path.',
595
+ code: 1
596
+ };
597
+ }
598
+
599
+ const expectedSha256 = getConfiguredExecutableSha256(pluginConfig);
600
+ const integrityResult = verifyExecutableIntegrity(executablePath, expectedSha256);
601
+ if (!integrityResult.ok) {
602
+ return {
603
+ success: false,
604
+ output: `Executable integrity verification failed for ${executablePath}.`,
605
+ code: 1
606
+ };
607
+ }
608
+
609
+ let sanitizedArgs;
546
610
  try {
547
- // Use execFileSync to avoid shell injection
548
- // Arguments are passed as array, not interpolated into shell
549
- const output = execFileSync('clawvault', args, {
611
+ sanitizedArgs = sanitizeExecArgs(args);
612
+ } catch (err) {
613
+ return {
614
+ success: false,
615
+ output: err?.message || 'Invalid command arguments',
616
+ code: 1
617
+ };
618
+ }
619
+
620
+ try {
621
+ const output = execFileSync(executablePath, sanitizedArgs, {
550
622
  encoding: 'utf-8',
551
623
  timeout: timeoutMs,
552
624
  stdio: ['pipe', 'pipe', 'pipe'],
553
- // Explicitly no shell
554
625
  shell: false
555
626
  });
556
627
  return { success: true, output: output.trim(), code: 0 };
@@ -588,23 +659,29 @@ function parseRecoveryOutput(output) {
588
659
  return { hadDeath, workingOn };
589
660
  }
590
661
 
591
- function resolveAgentIdForEvent(event) {
662
+ function resolveAgentIdForEvent(event, pluginConfig) {
592
663
  const fromSessionKey = extractAgentIdFromSessionKey(extractSessionKey(event));
593
664
  if (fromSessionKey) return fromSessionKey;
594
665
 
595
- const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
596
- if (fromEnv) return fromEnv;
666
+ if (allowsEnvAccess(pluginConfig)) {
667
+ const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
668
+ if (fromEnv) return fromEnv;
669
+ }
597
670
 
598
671
  return 'main';
599
672
  }
600
673
 
601
- function runObserverCron(vaultPath, agentId, options = {}) {
674
+ function runObserverCron(vaultPath, agentId, pluginConfig, options = {}) {
602
675
  const args = ['observe', '--cron', '--agent', agentId, '-v', vaultPath];
603
676
  if (Number.isFinite(options.minNewBytes) && Number(options.minNewBytes) > 0) {
604
677
  args.push('--min-new', String(Math.floor(Number(options.minNewBytes))));
605
678
  }
606
679
 
607
- const result = runClawvault(args, { timeoutMs: 120000 });
680
+ const result = runClawvault(args, pluginConfig, { timeoutMs: 120000 });
681
+ if (result.skipped) {
682
+ console.log('[clawvault] Observer cron skipped: allowClawvaultExec is disabled');
683
+ return false;
684
+ }
608
685
  if (!result.success) {
609
686
  console.warn(`[clawvault] Observer cron failed (${options.reason || 'unknown reason'})`);
610
687
  return false;
@@ -1314,7 +1391,12 @@ function isSundayMidnightUtc(date) {
1314
1391
  }
1315
1392
 
1316
1393
  async function handleWeeklyReflect(event) {
1317
- const vaultPath = findVaultPath(event);
1394
+ const pluginConfig = extractPluginConfig(event);
1395
+ if (!isOptInEnabled(pluginConfig, 'enableWeeklyReflection', 'weeklyReflection')) {
1396
+ return;
1397
+ }
1398
+
1399
+ const vaultPath = findVaultPath(event, pluginConfig);
1318
1400
  if (!vaultPath) {
1319
1401
  console.log('[clawvault] No vault found, skipping weekly reflection');
1320
1402
  return;
@@ -1326,7 +1408,11 @@ async function handleWeeklyReflect(event) {
1326
1408
  return;
1327
1409
  }
1328
1410
 
1329
- const result = runClawvault(['reflect', '-v', vaultPath], { timeoutMs: 120000 });
1411
+ const result = runClawvault(['reflect', '-v', vaultPath], pluginConfig, { timeoutMs: 120000 });
1412
+ if (result.skipped) {
1413
+ console.log('[clawvault] Weekly reflection skipped: allowClawvaultExec is disabled');
1414
+ return;
1415
+ }
1330
1416
  if (!result.success) {
1331
1417
  console.warn('[clawvault] Weekly reflection failed');
1332
1418
  return;
@@ -1336,7 +1422,12 @@ async function handleWeeklyReflect(event) {
1336
1422
 
1337
1423
  // Handle gateway startup - check for context death
1338
1424
  async function handleStartup(event) {
1339
- const vaultPath = findVaultPath(event);
1425
+ const pluginConfig = extractPluginConfig(event);
1426
+ if (!isOptInEnabled(pluginConfig, 'enableStartupRecovery')) {
1427
+ return;
1428
+ }
1429
+
1430
+ const vaultPath = findVaultPath(event, pluginConfig);
1340
1431
  if (!vaultPath) {
1341
1432
  console.log('[clawvault] No vault found, skipping recovery check');
1342
1433
  return;
@@ -1345,7 +1436,11 @@ async function handleStartup(event) {
1345
1436
  console.log(`[clawvault] Checking for context death`);
1346
1437
 
1347
1438
  // Pass vault path as separate argument (not interpolated)
1348
- const result = runClawvault(['recover', '--clear', '-v', vaultPath]);
1439
+ const result = runClawvault(['recover', '--clear', '-v', vaultPath], pluginConfig);
1440
+ if (result.skipped) {
1441
+ console.log('[clawvault] Recovery check skipped: allowClawvaultExec is disabled');
1442
+ return;
1443
+ }
1349
1444
 
1350
1445
  if (!result.success) {
1351
1446
  console.warn('[clawvault] Recovery check failed');
@@ -1375,7 +1470,15 @@ async function handleStartup(event) {
1375
1470
 
1376
1471
  // Handle /new command - auto-checkpoint before reset
1377
1472
  async function handleNew(event) {
1378
- const vaultPath = findVaultPath(event);
1473
+ const pluginConfig = extractPluginConfig(event);
1474
+ const autoCheckpointEnabled = isOptInEnabled(pluginConfig, 'enableAutoCheckpoint', 'autoCheckpoint');
1475
+ const observerOnNewEnabled = isOptInEnabled(pluginConfig, 'enableObserveOnNew');
1476
+ const factExtractionEnabled = isOptInEnabled(pluginConfig, 'enableFactExtraction');
1477
+ if (!autoCheckpointEnabled && !observerOnNewEnabled && !factExtractionEnabled) {
1478
+ return;
1479
+ }
1480
+
1481
+ const vaultPath = findVaultPath(event, pluginConfig);
1379
1482
  if (!vaultPath) {
1380
1483
  console.log('[clawvault] No vault found, skipping auto-checkpoint');
1381
1484
  return;
@@ -1389,33 +1492,44 @@ async function handleNew(event) {
1389
1492
  ? event.context.commandSource.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 50)
1390
1493
  : 'cli';
1391
1494
 
1392
- console.log('[clawvault] Auto-checkpoint before /new');
1393
-
1394
- // Pass each argument separately (no shell interpolation)
1395
- const result = runClawvault([
1396
- 'checkpoint',
1397
- '--working-on', `Session reset via /new from ${source}`,
1398
- '--focus', `Pre-reset checkpoint, session: ${sessionKey}`,
1399
- '-v', vaultPath
1400
- ]);
1495
+ if (autoCheckpointEnabled) {
1496
+ console.log('[clawvault] Auto-checkpoint before /new');
1497
+ const result = runClawvault([
1498
+ 'checkpoint',
1499
+ '--working-on', `Session reset via /new from ${source}`,
1500
+ '--focus', `Pre-reset checkpoint, session: ${sessionKey}`,
1501
+ '-v', vaultPath
1502
+ ], pluginConfig);
1401
1503
 
1402
- if (result.success) {
1403
- console.log('[clawvault] Auto-checkpoint created');
1404
- } else {
1405
- console.warn('[clawvault] Auto-checkpoint failed');
1504
+ if (result.skipped) {
1505
+ console.log('[clawvault] Auto-checkpoint skipped: allowClawvaultExec is disabled');
1506
+ } else if (result.success) {
1507
+ console.log('[clawvault] Auto-checkpoint created');
1508
+ } else {
1509
+ console.warn('[clawvault] Auto-checkpoint failed');
1510
+ }
1406
1511
  }
1407
1512
 
1408
- const agentId = resolveAgentIdForEvent(event);
1409
- runObserverCron(vaultPath, agentId, {
1410
- minNewBytes: 1,
1411
- reason: 'command:new flush'
1412
- });
1413
- runFactExtractionForEvent(vaultPath, event, 'command:new');
1513
+ const agentId = resolveAgentIdForEvent(event, pluginConfig);
1514
+ if (observerOnNewEnabled) {
1515
+ runObserverCron(vaultPath, agentId, pluginConfig, {
1516
+ minNewBytes: 1,
1517
+ reason: 'command:new flush'
1518
+ });
1519
+ }
1520
+ if (factExtractionEnabled) {
1521
+ runFactExtractionForEvent(vaultPath, event, 'command:new');
1522
+ }
1414
1523
  }
1415
1524
 
1416
1525
  // Handle session start - inject dynamic context for first prompt
1417
1526
  async function handleSessionStart(event) {
1418
- const vaultPath = findVaultPath(event);
1527
+ const pluginConfig = extractPluginConfig(event);
1528
+ if (!isOptInEnabled(pluginConfig, 'enableSessionContextInjection')) {
1529
+ return;
1530
+ }
1531
+
1532
+ const vaultPath = findVaultPath(event, pluginConfig);
1419
1533
  if (!vaultPath) {
1420
1534
  console.log('[clawvault] No vault found, skipping context injection');
1421
1535
  return;
@@ -1434,11 +1548,14 @@ async function handleSessionStart(event) {
1434
1548
  recapArgs.push('--agent', agentId);
1435
1549
  }
1436
1550
 
1437
- const recapResult = runClawvault(recapArgs);
1438
- if (!recapResult.success) {
1439
- console.warn('[clawvault] Session recap lookup failed');
1440
- } else {
1551
+ const recapResult = runClawvault(recapArgs, pluginConfig);
1552
+ if (recapResult.skipped) {
1553
+ console.log('[clawvault] Session recap skipped: allowClawvaultExec is disabled');
1554
+ }
1555
+ if (recapResult.success) {
1441
1556
  recapEntries = parseSessionRecapJson(recapResult.output);
1557
+ } else if (!recapResult.skipped) {
1558
+ console.warn('[clawvault] Session recap lookup failed');
1442
1559
  }
1443
1560
  } else {
1444
1561
  console.log('[clawvault] No session key found, skipping session recap');
@@ -1452,12 +1569,14 @@ async function handleSessionStart(event) {
1452
1569
  '--format', 'json',
1453
1570
  '--profile', 'auto',
1454
1571
  '-v', vaultPath
1455
- ]);
1572
+ ], pluginConfig);
1456
1573
 
1457
- if (!contextResult.success) {
1458
- console.warn('[clawvault] Context lookup failed');
1459
- } else {
1574
+ if (contextResult.success) {
1460
1575
  memoryEntries = parseContextJson(contextResult.output);
1576
+ } else if (contextResult.skipped) {
1577
+ console.log('[clawvault] Context lookup skipped: allowClawvaultExec is disabled');
1578
+ } else {
1579
+ console.warn('[clawvault] Context lookup failed');
1461
1580
  }
1462
1581
  } else {
1463
1582
  console.log('[clawvault] No initial prompt, skipping vault memory lookup');
@@ -1477,35 +1596,51 @@ async function handleSessionStart(event) {
1477
1596
 
1478
1597
  // Handle heartbeat events - cheap stat-based trigger for active observation
1479
1598
  async function handleHeartbeat(event) {
1480
- const vaultPath = findVaultPath(event);
1599
+ const pluginConfig = extractPluginConfig(event);
1600
+ if (!isOptInEnabled(pluginConfig, 'enableHeartbeatObservation', 'observeOnHeartbeat')) {
1601
+ return;
1602
+ }
1603
+
1604
+ const vaultPath = findVaultPath(event, pluginConfig);
1481
1605
  if (!vaultPath) {
1482
1606
  console.log('[clawvault] No vault found, skipping heartbeat observation check');
1483
1607
  return;
1484
1608
  }
1485
1609
 
1486
- const agentId = resolveAgentIdForEvent(event);
1487
- if (!shouldObserveActiveSessions(vaultPath, agentId)) {
1610
+ const agentId = resolveAgentIdForEvent(event, pluginConfig);
1611
+ if (!shouldObserveActiveSessions(vaultPath, agentId, pluginConfig)) {
1488
1612
  console.log('[clawvault] Heartbeat: no sessions crossed active-observe threshold');
1489
1613
  return;
1490
1614
  }
1491
1615
 
1492
- runObserverCron(vaultPath, agentId, { reason: 'heartbeat threshold crossed' });
1616
+ runObserverCron(vaultPath, agentId, pluginConfig, { reason: 'heartbeat threshold crossed' });
1493
1617
  }
1494
1618
 
1495
1619
  // Handle context compaction - force flush any pending session deltas
1496
1620
  async function handleContextCompaction(event) {
1497
- const vaultPath = findVaultPath(event);
1621
+ const pluginConfig = extractPluginConfig(event);
1622
+ const compactionObserveEnabled = isOptInEnabled(pluginConfig, 'enableCompactionObservation');
1623
+ const factExtractionEnabled = isOptInEnabled(pluginConfig, 'enableFactExtraction');
1624
+ if (!compactionObserveEnabled && !factExtractionEnabled) {
1625
+ return;
1626
+ }
1627
+
1628
+ const vaultPath = findVaultPath(event, pluginConfig);
1498
1629
  if (!vaultPath) {
1499
1630
  console.log('[clawvault] No vault found, skipping compaction observation');
1500
1631
  return;
1501
1632
  }
1502
1633
 
1503
- const agentId = resolveAgentIdForEvent(event);
1504
- runObserverCron(vaultPath, agentId, {
1505
- minNewBytes: 1,
1506
- reason: 'context compaction'
1507
- });
1508
- runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
1634
+ const agentId = resolveAgentIdForEvent(event, pluginConfig);
1635
+ if (compactionObserveEnabled) {
1636
+ runObserverCron(vaultPath, agentId, pluginConfig, {
1637
+ minNewBytes: 1,
1638
+ reason: 'context compaction'
1639
+ });
1640
+ }
1641
+ if (factExtractionEnabled) {
1642
+ runFactExtractionForEvent(vaultPath, event, 'compaction:memoryFlush');
1643
+ }
1509
1644
  }
1510
1645
 
1511
1646
  // Main handler - route events