33/**
44 * Auto-install Claude hooks during npm install
55 * This runs as a postinstall script to set up tracing hooks and daemon
6+ *
7+ * INTERACTIVE: Asks for user consent before modifying ~/.claude
68 */
79
810import {
@@ -14,6 +16,7 @@ import {
1416} from 'fs' ;
1517import { join } from 'path' ;
1618import { homedir } from 'os' ;
19+ import { createInterface } from 'readline' ;
1720
1821import { fileURLToPath } from 'url' ;
1922import { dirname } from 'path' ;
@@ -27,12 +30,63 @@ const templatesDir = join(__dirname, '..', 'templates', 'claude-hooks');
2730const stackmemoryBinDir = join ( homedir ( ) , '.stackmemory' , 'bin' ) ;
2831const distDir = join ( __dirname , '..' , 'dist' ) ;
2932
33+ /**
34+ * Ask user for confirmation before installing hooks
35+ * Returns true if user consents, false otherwise
36+ */
37+ async function askForConsent ( ) {
38+ // Skip prompt if:
39+ // 1. Not a TTY (CI/CD, piped input)
40+ // 2. STACKMEMORY_AUTO_HOOKS=true is set
41+ // 3. Running in CI environment
42+ if (
43+ ! process . stdin . isTTY ||
44+ process . env . STACKMEMORY_AUTO_HOOKS === 'true' ||
45+ process . env . CI === 'true'
46+ ) {
47+ // In non-interactive mode, skip hook installation silently
48+ console . log (
49+ 'StackMemory installed. Run "stackmemory setup-mcp" to configure Claude Code integration.'
50+ ) ;
51+ return false ;
52+ }
53+
54+ console . log ( '\n📦 StackMemory Post-Install\n' ) ;
55+ console . log (
56+ 'StackMemory can integrate with Claude Code by installing hooks that:'
57+ ) ;
58+ console . log ( ' - Track tool usage for better context' ) ;
59+ console . log ( ' - Enable session persistence across restarts' ) ;
60+ console . log ( ' - Sync context with Linear (optional)' ) ;
61+ console . log ( '\nThis will modify files in ~/.claude/\n' ) ;
62+
63+ return new Promise ( ( resolve ) => {
64+ const rl = createInterface ( {
65+ input : process . stdin ,
66+ output : process . stdout ,
67+ } ) ;
68+
69+ rl . question ( 'Install Claude Code hooks? [y/N] ' , ( answer ) => {
70+ rl . close ( ) ;
71+ const normalized = answer . toLowerCase ( ) . trim ( ) ;
72+ resolve ( normalized === 'y' || normalized === 'yes' ) ;
73+ } ) ;
74+
75+ // Timeout after 30 seconds - default to no
76+ setTimeout ( ( ) => {
77+ console . log ( '\n(Timed out, skipping hook installation)' ) ;
78+ rl . close ( ) ;
79+ resolve ( false ) ;
80+ } , 30000 ) ;
81+ } ) ;
82+ }
83+
3084async function installClaudeHooks ( ) {
3185 try {
3286 // Create Claude hooks directory if it doesn't exist
3387 if ( ! existsSync ( claudeHooksDir ) ) {
3488 mkdirSync ( claudeHooksDir , { recursive : true } ) ;
35- console . log ( '📁 Created Claude hooks directory' ) ;
89+ console . log ( 'Created ~/.claude/ hooks directory' ) ;
3690 }
3791
3892 // Copy hook files
@@ -48,7 +102,7 @@ async function installClaudeHooks() {
48102 if ( existsSync ( destPath ) ) {
49103 const backupPath = `${ destPath } .backup-${ Date . now ( ) } ` ;
50104 copyFileSync ( destPath , backupPath ) ;
51- console . log ( `📋 Backed up existing hook : ${ hookFile } ` ) ;
105+ console . log ( ` Backed up: ${ hookFile } ` ) ;
52106 }
53107
54108 copyFileSync ( srcPath , destPath ) ;
@@ -62,7 +116,7 @@ async function installClaudeHooks() {
62116 }
63117
64118 installed ++ ;
65- console . log ( `✅ Installed hook : ${ hookFile } ` ) ;
119+ console . log ( ` Installed : ${ hookFile } ` ) ;
66120 }
67121 }
68122
@@ -71,9 +125,8 @@ async function installClaudeHooks() {
71125 if ( existsSync ( claudeConfigFile ) ) {
72126 try {
73127 hooksConfig = JSON . parse ( readFileSync ( claudeConfigFile , 'utf8' ) ) ;
74- console . log ( '📋 Loaded existing hooks.json' ) ;
75128 } catch {
76- console . log ( '⚠️ Could not parse existing hooks.json, creating new' ) ;
129+ // Start fresh if parse fails
77130 }
78131 }
79132
@@ -86,32 +139,20 @@ async function installClaudeHooks() {
86139 } ;
87140
88141 writeFileSync ( claudeConfigFile , JSON . stringify ( newHooksConfig , null , 2 ) ) ;
89- console . log ( '🔧 Updated hooks.json configuration' ) ;
90142
91143 if ( installed > 0 ) {
92- console . log (
93- `\nSuccessfully installed ${ installed } Claude hooks for StackMemory tracing!`
94- ) ;
95- console . log (
96- 'Tool usage and session data will now be automatically logged'
97- ) ;
98- console . log (
99- `Traces saved to: ${ join ( homedir ( ) , '.stackmemory' , 'traces' ) } `
100- ) ;
101- console . log (
102- '\nTo disable tracing, set DEBUG_TRACE=false in your .env file'
103- ) ;
144+ console . log ( `\n[OK] Installed ${ installed } Claude hooks` ) ;
145+ console . log ( ` Traces: ~/.stackmemory/traces/` ) ;
146+ console . log ( ' To disable: set DEBUG_TRACE=false in .env' ) ;
104147 }
105148
106149 // Install session daemon binary
107150 await installSessionDaemon ( ) ;
108151
109152 return true ;
110153 } catch ( error ) {
111- console . error ( 'Failed to install Claude hooks:' , error . message ) ;
112- console . error (
113- ' This is not critical - StackMemory will still work without hooks'
114- ) ;
154+ console . error ( 'Hook installation failed:' , error . message ) ;
155+ console . error ( '(Non-critical - StackMemory works without hooks)' ) ;
115156 return false ;
116157 }
117158}
@@ -154,8 +195,17 @@ async function installSessionDaemon() {
154195
155196// Only run if called directly (not imported)
156197if ( import . meta. url === `file://${ process . argv [ 1 ] } ` ) {
157- console . log ( '🔧 Installing StackMemory Claude Code integration hooks...\n' ) ;
158- await installClaudeHooks ( ) ;
198+ const consent = await askForConsent ( ) ;
199+ if ( consent ) {
200+ await installClaudeHooks ( ) ;
201+ console . log (
202+ '\nNext: Run "stackmemory setup-mcp" to complete Claude Code integration.'
203+ ) ;
204+ } else {
205+ console . log (
206+ 'Skipped hook installation. Run "stackmemory hooks install" later if needed.'
207+ ) ;
208+ }
159209}
160210
161211export { installClaudeHooks } ;
0 commit comments