@@ -143,6 +143,106 @@ Hello`);
143143 expect ( context . frontmatter ) . toEqual ( { } ) ;
144144 expect ( context . rawBody ) . toBe ( "Just body" ) ;
145145 } ) ;
146+
147+ it ( "runs pre hook and captures output" , async ( ) => {
148+ const filePath = join ( tempDir , "prehook.claude.md" ) ;
149+ await writeFile ( filePath , `---
150+ pre: echo "pre-hook output"
151+ ---
152+ Body content` ) ;
153+
154+ const runtime = createRuntime ( ) ;
155+ const resolved = await runtime . resolve ( filePath ) ;
156+ const context = await runtime . buildContext ( resolved ) ;
157+
158+ expect ( context . preHookOutput ) . toBe ( "pre-hook output\n" ) ;
159+ } ) ;
160+
161+ it ( "runs before hook (alias for pre)" , async ( ) => {
162+ const filePath = join ( tempDir , "beforehook.claude.md" ) ;
163+ await writeFile ( filePath , `---
164+ before: echo "before-hook output"
165+ ---
166+ Body content` ) ;
167+
168+ const runtime = createRuntime ( ) ;
169+ const resolved = await runtime . resolve ( filePath ) ;
170+ const context = await runtime . buildContext ( resolved ) ;
171+
172+ expect ( context . preHookOutput ) . toBe ( "before-hook output\n" ) ;
173+ } ) ;
174+
175+ it ( "pre hook takes precedence over before" , async ( ) => {
176+ const filePath = join ( tempDir , "prebefore.claude.md" ) ;
177+ await writeFile ( filePath , `---
178+ pre: echo "pre wins"
179+ before: echo "before loses"
180+ ---
181+ Body content` ) ;
182+
183+ const runtime = createRuntime ( ) ;
184+ const resolved = await runtime . resolve ( filePath ) ;
185+ const context = await runtime . buildContext ( resolved ) ;
186+
187+ expect ( context . preHookOutput ) . toBe ( "pre wins\n" ) ;
188+ } ) ;
189+
190+ it ( "throws error when pre hook fails" , async ( ) => {
191+ const filePath = join ( tempDir , "badhook.claude.md" ) ;
192+ await writeFile ( filePath , `---
193+ pre: exit 1
194+ ---
195+ Body content` ) ;
196+
197+ const runtime = createRuntime ( ) ;
198+ const resolved = await runtime . resolve ( filePath ) ;
199+
200+ await expect ( runtime . buildContext ( resolved ) ) . rejects . toThrow ( "Pre hook failed" ) ;
201+ } ) ;
202+
203+ it ( "captures post hook command for later" , async ( ) => {
204+ const filePath = join ( tempDir , "posthook.claude.md" ) ;
205+ await writeFile ( filePath , `---
206+ post: echo "done"
207+ ---
208+ Body content` ) ;
209+
210+ const runtime = createRuntime ( ) ;
211+ const resolved = await runtime . resolve ( filePath ) ;
212+ const context = await runtime . buildContext ( resolved ) ;
213+
214+ expect ( context . postHookCommand ) . toBe ( "echo \"done\"" ) ;
215+ } ) ;
216+
217+ it ( "after hook is alias for post" , async ( ) => {
218+ const filePath = join ( tempDir , "afterhook.claude.md" ) ;
219+ await writeFile ( filePath , `---
220+ after: echo "after done"
221+ ---
222+ Body content` ) ;
223+
224+ const runtime = createRuntime ( ) ;
225+ const resolved = await runtime . resolve ( filePath ) ;
226+ const context = await runtime . buildContext ( resolved ) ;
227+
228+ expect ( context . postHookCommand ) . toBe ( "echo \"after done\"" ) ;
229+ } ) ;
230+
231+ it ( "pre hook uses env vars from frontmatter" , async ( ) => {
232+ const filePath = join ( tempDir , "hookenv.claude.md" ) ;
233+ await writeFile ( filePath , `---
234+ env:
235+ MY_VAR: hello
236+ pre: echo "$MY_VAR world"
237+ ---
238+ Body` ) ;
239+
240+ const runtime = createRuntime ( ) ;
241+ const resolved = await runtime . resolve ( filePath ) ;
242+ const context = await runtime . buildContext ( resolved ) ;
243+
244+ expect ( context . preHookOutput ) . toBe ( "hello world\n" ) ;
245+ } ) ;
146246 } ) ;
147247
148248 describe ( "Template Phase" , ( ) => {
@@ -324,6 +424,30 @@ Test prompt`);
324424 // Cleanup should not throw for local files
325425 await expect ( runtime . cleanup ( ) ) . resolves . toBeUndefined ( ) ;
326426 } ) ;
427+
428+ it ( "prepends pre hook output to body in dry run" , async ( ) => {
429+ const filePath = join ( tempDir , "preoutput.claude.md" ) ;
430+ await writeFile ( filePath , `---
431+ pre: echo "HOOK OUTPUT"
432+ ---
433+ Body content` ) ;
434+
435+ const runtime = createRuntime ( ) ;
436+ const resolved = await runtime . resolve ( filePath ) ;
437+ const context = await runtime . buildContext ( resolved ) ;
438+ const processed = await runtime . processTemplate ( context ) ;
439+
440+ // Verify pre-hook output is captured
441+ expect ( context . preHookOutput ) . toBe ( "HOOK OUTPUT\n" ) ;
442+
443+ // Simulate what execute does with pre-hook output
444+ let finalBody = processed . body ;
445+ if ( context . preHookOutput ) {
446+ finalBody = `${ context . preHookOutput . trim ( ) } \n\n${ finalBody } ` ;
447+ }
448+
449+ expect ( finalBody ) . toBe ( "HOOK OUTPUT\n\nBody content" ) ;
450+ } ) ;
327451 } ) ;
328452
329453 describe ( "createRuntime factory" , ( ) => {
0 commit comments