amalgm 0.1.47 → 0.1.49

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "amalgm",
3
- "version": "0.1.47",
3
+ "version": "0.1.49",
4
4
  "description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -8,7 +8,7 @@ const { normalizeClaudeMessage, usageRecordsFromClaudeResult, usageFromClaude }
8
8
  const { recordNativeEvent } = require('../recorder');
9
9
  const { toClaudeMcpServers } = require('../tooling/mcp-bundle');
10
10
  const { bundledClaudeBinary } = require('../tooling/native-binaries');
11
- const { syncNativeHarnessConfig } = require('../tooling/native-config');
11
+ const { claudeNativeHookSettings } = require('../tooling/native-config');
12
12
  const { importPackage } = require('../tooling/package-import');
13
13
  const { composeSystemPrompt } = require('../tooling/system-prompt');
14
14
 
@@ -32,15 +32,18 @@ class ClaudeAdapter {
32
32
  }
33
33
 
34
34
  options(contract, extra = {}) {
35
- syncNativeHarnessConfig(contract);
36
35
  const systemPrompt = composeSystemPrompt(contract);
37
36
  const pathToClaudeCodeExecutable = process.env.CLAUDE_CODE_BINARY || bundledClaudeBinary();
37
+ const settings = {
38
+ ...(claudeNativeHookSettings({ cwd: contract.cwd }) || {}),
39
+ ...(contract.fastMode ? { fastMode: true, fastModePerSessionOptIn: true } : {}),
40
+ };
38
41
  return {
39
42
  cwd: contract.cwd,
40
43
  env: runtimeEnv(contract),
41
44
  model: contract.cliModel,
42
45
  ...(contract.reasoningEffort ? { effort: contract.reasoningEffort } : {}),
43
- ...(contract.fastMode ? { settings: { fastMode: true, fastModePerSessionOptIn: true } } : {}),
46
+ ...(Object.keys(settings).length > 0 ? { settings } : {}),
44
47
  ...(pathToClaudeCodeExecutable ? { pathToClaudeCodeExecutable } : {}),
45
48
  ...(systemPrompt ? { systemPrompt: { type: 'preset', preset: 'claude_code', append: systemPrompt } } : {}),
46
49
  mcpServers: toClaudeMcpServers(contract),
@@ -49,7 +52,7 @@ class ClaudeAdapter {
49
52
  ...(process.env.CHAT_CORE_DEBUG_CLAUDE === '1'
50
53
  ? { debug: true, debugFile: path.join(contract.auth.runtimeHome || process.cwd(), 'claude-debug.log') }
51
54
  : {}),
52
- settingSources: ['user', 'project', 'local'],
55
+ settingSources: [],
53
56
  strictMcpConfig: false,
54
57
  ...extra,
55
58
  };
@@ -263,6 +263,10 @@ function hookTrustBlocks(toml) {
263
263
  return blocks;
264
264
  }
265
265
 
266
+ function hookTrustToml(toml) {
267
+ return hookTrustBlocks(toml).map((block) => block.lines.join('\n')).join('\n\n');
268
+ }
269
+
266
270
  function mirrorCodexHookTrust(toml, sourceDir, runtimeHome) {
267
271
  if (!sourceDir || !runtimeHome) return toml;
268
272
  const sourceHookPrefix = `${path.join(sourceDir, 'hooks.json')}:`;
@@ -298,7 +302,7 @@ function generatedMcpSectionNames(contract) {
298
302
  function buildCodexConfig(contract, existingConfig, syncInfo) {
299
303
  const mcpToml = toCodexMcpToml(contract);
300
304
  const topLevelKeys = [
301
- ...(contract.authMethod === 'provider_auth' ? [] : ['model_provider']),
305
+ 'model_provider',
302
306
  ...(contract.authMethod === 'amalgm' ? ['model_context_window', 'model_auto_compact_token_limit'] : []),
303
307
  ];
304
308
  let config = removeTopLevelKeys(existingConfig, topLevelKeys);
@@ -324,6 +328,8 @@ function buildCodexConfig(contract, existingConfig, syncInfo) {
324
328
  ].join('\n')
325
329
  : contract.authMethod === 'byok'
326
330
  ? 'model_provider = "openai"'
331
+ : contract.authMethod === 'provider_auth'
332
+ ? 'model_provider = "openai"'
327
333
  : '';
328
334
  return [
329
335
  generated.trimEnd(),
@@ -337,18 +343,19 @@ function writeConfig(contract) {
337
343
  if (!home) return;
338
344
  fs.mkdirSync(home, { recursive: true });
339
345
  const syncInfo = syncCodexNativeConfig(home);
346
+ const nativeHookTrust = hookTrustToml(readTextFile(syncInfo?.sourceConfigPath));
340
347
  const configPath = path.join(home, 'config.toml');
341
348
  if (contract.authMethod === 'provider_auth') {
342
- const sourceAuth = path.join(os.homedir(), '.codex', 'auth.json');
349
+ const sourceAuth = path.join(syncInfo?.sourceDir || path.join(os.homedir(), '.codex'), 'auth.json');
343
350
  const targetAuth = path.join(home, 'auth.json');
344
351
  if (fs.existsSync(sourceAuth)) {
345
352
  fs.copyFileSync(sourceAuth, targetAuth);
346
353
  fs.chmodSync(targetAuth, 0o600);
347
354
  }
348
- fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
355
+ fs.writeFileSync(configPath, buildCodexConfig(contract, nativeHookTrust, syncInfo), { mode: 0o600 });
349
356
  return;
350
357
  }
351
- fs.writeFileSync(configPath, buildCodexConfig(contract, readTextFile(configPath), syncInfo), { mode: 0o600 });
358
+ fs.writeFileSync(configPath, buildCodexConfig(contract, nativeHookTrust, syncInfo), { mode: 0o600 });
352
359
  fs.writeFileSync(path.join(home, 'auth.json'), JSON.stringify({
353
360
  auth_mode: 'apikey',
354
361
  OPENAI_API_KEY: contract.auth.tokenRef,
@@ -488,6 +495,7 @@ module.exports = {
488
495
  __private: {
489
496
  buildCodexConfig,
490
497
  ensureFeatureBoolean,
498
+ hookTrustToml,
491
499
  mirrorCodexHookTrust,
492
500
  modelWindowConfigLines,
493
501
  removeTomlSections,
@@ -70,7 +70,9 @@ function coerceAuth(harness, requested) {
70
70
  }
71
71
 
72
72
  function runtimeHome({ amalgmDir, sessionId, harness, authMethod, tokenFingerprint }) {
73
- if (authMethod === 'provider_auth' && !['claude_code', 'codex'].includes(harness)) return null;
73
+ // Claude Code provider auth is tied to the user's real home/keychain context.
74
+ // Isolating HOME makes the bundled Claude binary look logged out on macOS.
75
+ if (authMethod === 'provider_auth' && harness !== 'codex') return null;
74
76
  const suffix = tokenFingerprint || 'default';
75
77
  return path.join(amalgmDir, 'runtime', sessionId, harness, suffix);
76
78
  }
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const test = require('node:test');
5
+ const { authEnvelope, runtimeEnv } = require('../auth');
6
+
7
+ test('claude provider auth keeps the user home environment', () => {
8
+ const envelope = authEnvelope({
9
+ harness: 'claude_code',
10
+ authMethod: 'provider_auth',
11
+ sessionId: 'session-test',
12
+ amalgmDir: '/tmp/amalgm-test',
13
+ });
14
+
15
+ assert.equal(envelope.runtimeHome, null);
16
+
17
+ const env = runtimeEnv({
18
+ harness: 'claude_code',
19
+ authMethod: 'provider_auth',
20
+ auth: envelope,
21
+ }, {
22
+ HOME: '/Users/example',
23
+ PATH: '/bin',
24
+ CLAUDE_CONFIG_DIR: '/Users/example/.claude',
25
+ ANTHROPIC_API_KEY: 'should-not-leak',
26
+ });
27
+
28
+ assert.equal(env.HOME, '/Users/example');
29
+ assert.equal(env.CLAUDE_CONFIG_DIR, '/Users/example/.claude');
30
+ assert.equal(env.ANTHROPIC_API_KEY, undefined);
31
+ });
32
+
33
+ test('codex provider auth still receives an isolated runtime home', () => {
34
+ const envelope = authEnvelope({
35
+ harness: 'codex',
36
+ authMethod: 'provider_auth',
37
+ sessionId: 'session-test',
38
+ amalgmDir: '/tmp/amalgm-test',
39
+ });
40
+
41
+ assert.match(envelope.runtimeHome, /\/tmp\/amalgm-test\/runtime\/session-test\/codex\/[^/]+$/);
42
+ });
@@ -0,0 +1,127 @@
1
+ 'use strict';
2
+
3
+ const assert = require('node:assert/strict');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const test = require('node:test');
8
+ const { ClaudeAdapter } = require('../adapters/claude');
9
+ const { __private: codexPrivate } = require('../adapters/codex');
10
+ const {
11
+ claudeNativeHookSettings,
12
+ syncCodexNativeConfig,
13
+ } = require('../tooling/native-config');
14
+
15
+ function withNativeHome(fn) {
16
+ const previousNativeHome = process.env.AMALGM_NATIVE_HOME;
17
+ const previousHome = process.env.HOME;
18
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'amalgm-native-config-'));
19
+ process.env.AMALGM_NATIVE_HOME = root;
20
+ process.env.HOME = root;
21
+ try {
22
+ return fn(root);
23
+ } finally {
24
+ if (previousNativeHome === undefined) delete process.env.AMALGM_NATIVE_HOME;
25
+ else process.env.AMALGM_NATIVE_HOME = previousNativeHome;
26
+ if (previousHome === undefined) delete process.env.HOME;
27
+ else process.env.HOME = previousHome;
28
+ fs.rmSync(root, { recursive: true, force: true });
29
+ }
30
+ }
31
+
32
+ test('codex native sync copies hook support without bulk runtime state', () => {
33
+ withNativeHome((home) => {
34
+ const source = path.join(home, '.codex');
35
+ fs.mkdirSync(path.join(source, 'sessions'), { recursive: true });
36
+ fs.mkdirSync(path.join(source, 'worktrees'), { recursive: true });
37
+ fs.mkdirSync(path.join(source, 'plugins'), { recursive: true });
38
+ fs.mkdirSync(path.join(source, 'supermemory'), { recursive: true });
39
+ fs.writeFileSync(path.join(source, 'hooks.json'), '{"hooks":{}}');
40
+ fs.writeFileSync(path.join(source, 'supermemory.json'), '{"projectContainerTag":"test"}');
41
+ fs.writeFileSync(path.join(source, 'supermemory', 'recall.js'), 'console.log("recall")');
42
+ fs.writeFileSync(path.join(source, 'sessions', 'huge.jsonl'), 'nope');
43
+ fs.writeFileSync(path.join(source, 'worktrees', 'tree'), 'nope');
44
+ fs.writeFileSync(path.join(source, 'plugins', 'cache'), 'nope');
45
+
46
+ const runtimeHome = path.join(home, 'runtime-home');
47
+ fs.mkdirSync(path.join(runtimeHome, 'sessions'), { recursive: true });
48
+ fs.writeFileSync(path.join(runtimeHome, 'sessions', 'legacy.jsonl'), 'old');
49
+
50
+ const result = syncCodexNativeConfig(runtimeHome);
51
+
52
+ assert.equal(result.sourceDir, source);
53
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'hooks.json')), true);
54
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory.json')), true);
55
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'supermemory', 'recall.js')), true);
56
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'sessions')), false);
57
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'worktrees')), false);
58
+ assert.equal(fs.existsSync(path.join(runtimeHome, 'plugins')), false);
59
+ assert.equal(fs.existsSync(path.join(runtimeHome, '.codex', 'hooks.json')), true);
60
+ });
61
+ });
62
+
63
+ test('codex provider config keeps hooks but drops native model and mcp config', () => {
64
+ withNativeHome((home) => {
65
+ const source = path.join(home, '.codex');
66
+ fs.mkdirSync(source, { recursive: true });
67
+ const hooksPath = path.join(source, 'hooks.json');
68
+ fs.writeFileSync(hooksPath, '{"hooks":{"UserPromptSubmit":[]}}');
69
+ fs.writeFileSync(path.join(source, 'auth.json'), '{"auth_mode":"chatgpt"}');
70
+ fs.writeFileSync(path.join(source, 'config.toml'), [
71
+ 'model_provider = "native-provider"',
72
+ '',
73
+ '[mcp_servers.native]',
74
+ 'command = "native-mcp"',
75
+ '',
76
+ `[hooks.state.${JSON.stringify(`${hooksPath}:UserPromptSubmit:0`)}]`,
77
+ 'trusted = true',
78
+ '',
79
+ ].join('\n'));
80
+
81
+ const runtimeHome = path.join(home, 'runtime-home');
82
+ codexPrivate.writeConfig({
83
+ authMethod: 'provider_auth',
84
+ auth: { runtimeHome },
85
+ mcpServers: [],
86
+ });
87
+
88
+ const config = fs.readFileSync(path.join(runtimeHome, 'config.toml'), 'utf8');
89
+ assert.match(config, /model_provider = "openai"/);
90
+ assert.match(config, /codex_hooks = true/);
91
+ assert.doesNotMatch(config, /native-provider/);
92
+ assert.doesNotMatch(config, /mcp_servers\.native/);
93
+ assert.equal(config.includes(path.join(runtimeHome, 'hooks.json')), true);
94
+ });
95
+ });
96
+
97
+ test('claude extracts native hooks without enabling full filesystem settings', () => {
98
+ withNativeHome((home) => {
99
+ const source = path.join(home, '.claude');
100
+ fs.mkdirSync(source, { recursive: true });
101
+ fs.writeFileSync(path.join(source, 'settings.json'), JSON.stringify({
102
+ permissions: { allow: ['Bash(*)'] },
103
+ hooks: {
104
+ UserPromptSubmit: [{
105
+ hooks: [{ type: 'command', command: 'echo recall', timeout: 1 }],
106
+ }],
107
+ },
108
+ }));
109
+
110
+ const hookSettings = claudeNativeHookSettings({ cwd: home });
111
+ assert.deepEqual(Object.keys(hookSettings.hooks), ['UserPromptSubmit']);
112
+ assert.equal(hookSettings.permissions, undefined);
113
+
114
+ const adapter = new ClaudeAdapter();
115
+ const options = adapter.options({
116
+ authMethod: 'provider_auth',
117
+ auth: { runtimeHome: null },
118
+ cwd: home,
119
+ cliModel: 'anthropic/claude-opus-4.7',
120
+ mcpServers: [],
121
+ });
122
+
123
+ assert.deepEqual(options.settingSources, []);
124
+ assert.equal(options.settings.permissions, undefined);
125
+ assert.equal(options.settings.hooks.UserPromptSubmit[0].hooks[0].command, 'echo recall');
126
+ });
127
+ });
@@ -9,12 +9,18 @@ const EXCLUDED_DIR_NAMES = new Set([
9
9
  '.npm',
10
10
  '.tmp',
11
11
  'cache',
12
+ 'generated_images',
12
13
  'log',
13
14
  'logs',
15
+ 'memories',
16
+ 'node_modules',
17
+ 'plugins',
14
18
  'projects',
15
19
  'sessions',
16
20
  'shell_snapshots',
21
+ 'skills',
17
22
  'tmp',
23
+ 'worktrees',
18
24
  ]);
19
25
 
20
26
  const EXCLUDED_FILE_PATTERNS = [
@@ -66,8 +72,8 @@ function copyFileIfPresent(source, target) {
66
72
  return true;
67
73
  }
68
74
 
69
- function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
70
- if (!runtimeHome || !dotDirName || !sourceDir || !exists(sourceDir)) return;
75
+ function ensureHomeAlias(runtimeHome, dotDirName) {
76
+ if (!runtimeHome || !dotDirName) return;
71
77
  const alias = path.join(runtimeHome, dotDirName);
72
78
  try {
73
79
  const stat = fs.lstatSync(alias);
@@ -75,9 +81,6 @@ function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
75
81
  const target = fs.readlinkSync(alias);
76
82
  if (target === '.' || path.resolve(runtimeHome, target) === path.resolve(runtimeHome)) return;
77
83
  fs.rmSync(alias, { recursive: true, force: true });
78
- } else if (stat.isDirectory()) {
79
- copyConfigTree(sourceDir, alias);
80
- return;
81
84
  } else {
82
85
  fs.rmSync(alias, { recursive: true, force: true });
83
86
  }
@@ -85,34 +88,138 @@ function copyIntoHomeAlias(runtimeHome, dotDirName, sourceDir) {
85
88
  try {
86
89
  fs.symlinkSync('.', alias, 'dir');
87
90
  } catch {
88
- copyConfigTree(sourceDir, alias);
91
+ fs.mkdirSync(alias, { recursive: true });
92
+ }
93
+ }
94
+
95
+ function nativeHome() {
96
+ return process.env.AMALGM_NATIVE_HOME || os.homedir();
97
+ }
98
+
99
+ function pruneLegacyCodexRuntimeHome(runtimeHome) {
100
+ for (const name of EXCLUDED_DIR_NAMES) {
101
+ try {
102
+ fs.rmSync(path.join(runtimeHome, name), { recursive: true, force: true });
103
+ } catch {}
89
104
  }
90
105
  }
91
106
 
107
+ function copyDirBounded(sourceDir, targetDir, options = {}) {
108
+ if (!sourceDir || !targetDir || !exists(sourceDir)) return { copied: false, files: 0, bytes: 0, truncated: false };
109
+ const maxFiles = Number(options.maxFiles || 200);
110
+ const maxBytes = Number(options.maxBytes || 10 * 1024 * 1024);
111
+ const state = { copied: false, files: 0, bytes: 0, truncated: false };
112
+ const root = path.resolve(sourceDir);
113
+
114
+ function walk(source, target) {
115
+ if (state.truncated || !shouldCopyConfigPath(root, source)) return;
116
+ let stat;
117
+ try {
118
+ stat = fs.lstatSync(source);
119
+ } catch {
120
+ return;
121
+ }
122
+ if (stat.isSymbolicLink()) return;
123
+ if (stat.isDirectory()) {
124
+ fs.mkdirSync(target, { recursive: true });
125
+ for (const entry of fs.readdirSync(source)) {
126
+ walk(path.join(source, entry), path.join(target, entry));
127
+ if (state.truncated) break;
128
+ }
129
+ return;
130
+ }
131
+ if (!stat.isFile()) return;
132
+ if (state.files + 1 > maxFiles || state.bytes + stat.size > maxBytes) {
133
+ state.truncated = true;
134
+ return;
135
+ }
136
+ fs.mkdirSync(path.dirname(target), { recursive: true });
137
+ fs.copyFileSync(source, target);
138
+ state.files += 1;
139
+ state.bytes += stat.size;
140
+ state.copied = true;
141
+ }
142
+
143
+ walk(sourceDir, targetDir);
144
+ return state;
145
+ }
146
+
92
147
  function syncCodexNativeConfig(runtimeHome) {
93
148
  if (!runtimeHome) return null;
94
- const nativeHome = os.homedir();
95
- const sourceDir = path.join(nativeHome, '.codex');
149
+ const sourceDir = path.join(nativeHome(), '.codex');
96
150
  fs.mkdirSync(runtimeHome, { recursive: true });
97
- const copied = copyConfigTree(sourceDir, runtimeHome);
98
- copyIntoHomeAlias(runtimeHome, '.codex', sourceDir);
99
- copyConfigTree(path.join(nativeHome, '.codex-supermemory'), path.join(runtimeHome, '.codex-supermemory'));
100
- copyFileIfPresent(path.join(nativeHome, '.codex-supermemory.log'), path.join(runtimeHome, '.codex-supermemory.log'));
101
- return copied ? { sourceDir, runtimeHome } : null;
151
+ if (!exists(sourceDir)) return null;
152
+
153
+ pruneLegacyCodexRuntimeHome(runtimeHome);
154
+ const copiedFiles = [
155
+ copyFileIfPresent(path.join(sourceDir, 'hooks.json'), path.join(runtimeHome, 'hooks.json')),
156
+ copyFileIfPresent(path.join(sourceDir, 'supermemory.json'), path.join(runtimeHome, 'supermemory.json')),
157
+ ].filter(Boolean).length;
158
+ const supermemory = copyDirBounded(path.join(sourceDir, 'supermemory'), path.join(runtimeHome, 'supermemory'));
159
+ copyDirBounded(path.join(nativeHome(), '.codex-supermemory'), path.join(runtimeHome, '.codex-supermemory'));
160
+ ensureHomeAlias(runtimeHome, '.codex');
161
+
162
+ return {
163
+ sourceDir,
164
+ runtimeHome,
165
+ sourceConfigPath: path.join(sourceDir, 'config.toml'),
166
+ copied: copiedFiles + (supermemory.copied ? 1 : 0) > 0,
167
+ truncated: supermemory.truncated,
168
+ };
102
169
  }
103
170
 
104
171
  function syncClaudeNativeConfig(runtimeHome) {
105
172
  if (!runtimeHome) return null;
106
- const nativeHome = os.homedir();
107
- const sourceDir = path.join(nativeHome, '.claude');
173
+ const sourceDir = path.join(nativeHome(), '.claude');
108
174
  fs.mkdirSync(runtimeHome, { recursive: true });
109
175
  const copied = copyConfigTree(sourceDir, runtimeHome);
110
- copyIntoHomeAlias(runtimeHome, '.claude', sourceDir);
111
- copyFileIfPresent(path.join(nativeHome, '.claude.json'), path.join(runtimeHome, '.claude.json'));
112
- copyConfigTree(path.join(nativeHome, '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
176
+ ensureHomeAlias(runtimeHome, '.claude');
177
+ copyFileIfPresent(path.join(nativeHome(), '.claude.json'), path.join(runtimeHome, '.claude.json'));
178
+ copyConfigTree(path.join(nativeHome(), '.config', 'claude'), path.join(runtimeHome, '.config', 'claude'));
113
179
  return copied ? { sourceDir, runtimeHome } : null;
114
180
  }
115
181
 
182
+ function readJsonFile(file) {
183
+ try {
184
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+
190
+ function cloneJson(value) {
191
+ if (value == null) return value;
192
+ try {
193
+ return JSON.parse(JSON.stringify(value));
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
198
+
199
+ function mergeHookMaps(target, hooks) {
200
+ if (!hooks || typeof hooks !== 'object' || Array.isArray(hooks)) return;
201
+ for (const [event, matchers] of Object.entries(hooks)) {
202
+ if (!Array.isArray(matchers)) continue;
203
+ const cloned = cloneJson(matchers);
204
+ if (!Array.isArray(cloned)) continue;
205
+ target[event] = [...(Array.isArray(target[event]) ? target[event] : []), ...cloned];
206
+ }
207
+ }
208
+
209
+ function claudeNativeHookSettings(options = {}) {
210
+ const sources = [
211
+ path.join(nativeHome(), '.claude', 'settings.json'),
212
+ options.cwd ? path.join(options.cwd, '.claude', 'settings.json') : null,
213
+ options.cwd ? path.join(options.cwd, '.claude', 'settings.local.json') : null,
214
+ ].filter(Boolean);
215
+ const hooks = {};
216
+ for (const source of sources) {
217
+ const data = readJsonFile(source);
218
+ mergeHookMaps(hooks, data?.hooks);
219
+ }
220
+ return Object.keys(hooks).length > 0 ? { hooks } : null;
221
+ }
222
+
116
223
  function syncNativeHarnessConfig(contract) {
117
224
  const runtimeHome = contract?.auth?.runtimeHome;
118
225
  if (!runtimeHome) return null;
@@ -123,10 +230,14 @@ function syncNativeHarnessConfig(contract) {
123
230
 
124
231
  module.exports = {
125
232
  __private: {
233
+ claudeNativeHookSettings,
126
234
  copyConfigTree,
235
+ copyDirBounded,
127
236
  copyFileIfPresent,
237
+ ensureHomeAlias,
128
238
  shouldCopyConfigPath,
129
239
  },
240
+ claudeNativeHookSettings,
130
241
  syncClaudeNativeConfig,
131
242
  syncCodexNativeConfig,
132
243
  syncNativeHarnessConfig,