@@ -60,6 +60,13 @@ const metadataDenyRule = "Read(./.entire/metadata/**)"
6060// repository root when it runs hooks.
6161const localDevHookCmdPrefix = "${CLAUDE_PROJECT_DIR}/scripts/entire-dev "
6262
63+ // localDevSessionEndTimeoutSecs gives the local-dev SessionEnd hook an explicit
64+ // timeout (seconds) so Claude Code waits for it on exit instead of cancelling it
65+ // after its short default exit-grace, which the build-from-source dev launcher
66+ // (scripts/entire-dev) can exceed. Only set in local-dev mode; production leaves
67+ // Claude Code's default in place.
68+ const localDevSessionEndTimeoutSecs = 60
69+
6370// entireHookPrefixes are command prefixes that identify Entire hooks. The
6471// "go run" prefix is retained so hooks installed by older versions are still
6572// recognized for removal/upgrade.
@@ -168,33 +175,41 @@ func (c *ClaudeCodeAgent) InstallHooks(ctx context.Context, localDev bool, force
168175
169176 count := 0
170177
178+ // The local-dev SessionEnd hook gets an explicit timeout so Claude Code
179+ // waits for it on exit; every other hook (and all production hooks) keeps
180+ // Claude Code's default.
181+ sessionEndTimeoutSecs := 0
182+ if localDev {
183+ sessionEndTimeoutSecs = localDevSessionEndTimeoutSecs
184+ }
185+
171186 // Add hooks if they don't exist
172187 if ! hookCommandExists (sessionStart , sessionStartCmd ) {
173- sessionStart = addHookToMatcher (sessionStart , "" , sessionStartCmd )
188+ sessionStart = addHookToMatcher (sessionStart , "" , sessionStartCmd , 0 )
174189 count ++
175190 }
176191 if ! hookCommandExists (sessionEnd , sessionEndCmd ) {
177- sessionEnd = addHookToMatcher (sessionEnd , "" , sessionEndCmd )
192+ sessionEnd = addHookToMatcher (sessionEnd , "" , sessionEndCmd , sessionEndTimeoutSecs )
178193 count ++
179194 }
180195 if ! hookCommandExists (stop , stopCmd ) {
181- stop = addHookToMatcher (stop , "" , stopCmd )
196+ stop = addHookToMatcher (stop , "" , stopCmd , 0 )
182197 count ++
183198 }
184199 if ! hookCommandExists (userPromptSubmit , userPromptSubmitCmd ) {
185- userPromptSubmit = addHookToMatcher (userPromptSubmit , "" , userPromptSubmitCmd )
200+ userPromptSubmit = addHookToMatcher (userPromptSubmit , "" , userPromptSubmitCmd , 0 )
186201 count ++
187202 }
188203 if ! hookCommandExistsWithMatcher (preToolUse , subagentToolMatcher , preTaskCmd ) {
189- preToolUse = addHookToMatcher (preToolUse , subagentToolMatcher , preTaskCmd )
204+ preToolUse = addHookToMatcher (preToolUse , subagentToolMatcher , preTaskCmd , 0 )
190205 count ++
191206 }
192207 if ! hookCommandExistsWithMatcher (postToolUse , subagentToolMatcher , postTaskCmd ) {
193- postToolUse = addHookToMatcher (postToolUse , subagentToolMatcher , postTaskCmd )
208+ postToolUse = addHookToMatcher (postToolUse , subagentToolMatcher , postTaskCmd , 0 )
194209 count ++
195210 }
196211 if ! hookCommandExistsWithMatcher (postToolUse , taskToolMatcher , postTodoCmd ) {
197- postToolUse = addHookToMatcher (postToolUse , taskToolMatcher , postTodoCmd )
212+ postToolUse = addHookToMatcher (postToolUse , taskToolMatcher , postTodoCmd , 0 )
198213 count ++
199214 }
200215
@@ -545,10 +560,11 @@ func hookCommandExistsWithMatcher(matchers []ClaudeHookMatcher, matcherName, com
545560 return false
546561}
547562
548- func addHookToMatcher (matchers []ClaudeHookMatcher , matcherName , command string ) []ClaudeHookMatcher {
563+ func addHookToMatcher (matchers []ClaudeHookMatcher , matcherName , command string , timeoutSecs int ) []ClaudeHookMatcher {
549564 entry := ClaudeHookEntry {
550565 Type : "command" ,
551566 Command : command ,
567+ Timeout : timeoutSecs ,
552568 }
553569
554570 // If no matcher name, add to a matcher with empty string
0 commit comments