@@ -124,15 +124,21 @@ function loadSeen(p) {
124124 } catch { return { } ; }
125125}
126126
127- function nudgeOnce ( key , message , ttl = NUDGE_TTL_MS ) {
127+ function codexWarningsEnabled ( ) {
128+ const value = ( process . env . TOKENLEAN_CODEX_WARNINGS || '' ) . toLowerCase ( ) ;
129+ return value === '1' || value === 'true' || value === 'yes' || value === 'on' ;
130+ }
131+
132+ function nudgeOnce ( key , message , ttl = NUDGE_TTL_MS , format = 'claude' ) {
133+ if ( format === 'codex' && ! codexWarningsEnabled ( ) ) return ;
128134 const p = getSeenPath ( ) ;
129135 if ( p ) {
130136 const seen = loadSeen ( p ) ;
131137 if ( seen [ key ] && ( Date . now ( ) - seen [ key ] ) < ttl ) return ;
132138 seen [ key ] = Date . now ( ) ;
133139 writeFileSync ( p , JSON . stringify ( seen ) , 'utf8' ) ;
134140 }
135- console . log ( makeNudge ( message ) ) ;
141+ console . log ( makeNudge ( message , format ) ) ;
136142}
137143
138144// --- Hook runner ---
@@ -154,13 +160,21 @@ function detectHookFormat(data) {
154160 if ( process . env . PI_CODING_AGENT ) return 'pi' ;
155161 if ( process . env . CLAUDE_CONFIG_DIR || process . env . CLAUDE_PROJECT_DIR ) return 'claude' ;
156162 if ( process . env . CODEX_THREAD_ID || process . env . CODEX_CI ) return 'codex' ;
163+ if ( data ?. hook_event_name && data ?. turn_id && data ?. permission_mode && data ?. session_id ) {
164+ return 'codex' ;
165+ }
157166 // Detect codex from payload transcript path when env vars aren't set
158167 if ( data ?. transcript_path ?. includes ( '/.codex/' ) ) return 'codex' ;
159168 return 'claude' ;
160169}
161170
162- function makeNudge ( message ) {
163- // Both Claude Code and Codex use the same hookSpecificOutput format
171+ function makeNudge ( message , format = 'claude' ) {
172+ if ( format === 'codex' ) {
173+ return JSON . stringify ( {
174+ systemMessage : message ,
175+ } ) ;
176+ }
177+
164178 return JSON . stringify ( {
165179 hookSpecificOutput : {
166180 hookEventName : 'PreToolUse' ,
@@ -232,6 +246,7 @@ async function runHook() {
232246 const format = detectHookFormat ( data ) ;
233247 const toolName = data . tool_name ;
234248 const toolInput = data . tool_input || { } ;
249+ const nudge = ( key , message , ttl = NUDGE_TTL_MS ) => nudgeOnce ( key , message , ttl , format ) ;
235250
236251 // --- Read on large files ---
237252 if ( toolName === 'Read' ) {
@@ -245,15 +260,15 @@ async function runHook() {
245260
246261 // Large JSON/YAML — worth flagging even though they're data files
247262 if ( size > LARGE_JSON_BYTES && ( ext === 'json' || ext === 'yaml' || ext === 'yml' ) ) {
248- nudgeOnce ( 'read-large-json' , `[tl] ${ Math . round ( size / 1024 ) } KB ${ ext } — use -j flag or tl-snippet to extract a specific path` , TTL_HIGH ) ;
263+ nudge ( 'read-large-json' , `[tl] ${ Math . round ( size / 1024 ) } KB ${ ext } — use -j flag or tl-snippet to extract a specific path` , TTL_HIGH ) ;
249264 return ;
250265 }
251266
252267 if ( NON_CODE_EXTS . has ( ext ) ) return ;
253268
254269 if ( size > LARGE_FILE_BYTES ) {
255270 const savedKb = Math . round ( size * 0.85 / 1024 ) ;
256- nudgeOnce ( 'read-large' , `[tl] ${ Math . round ( size / 1024 ) } KB — tl-symbols + tl-snippet saves ~${ savedKb } KB` , TTL_HIGH ) ;
271+ nudge ( 'read-large' , `[tl] ${ Math . round ( size / 1024 ) } KB — tl-symbols + tl-snippet saves ~${ savedKb } KB` , TTL_HIGH ) ;
257272 }
258273 return ;
259274 }
@@ -267,34 +282,34 @@ async function runHook() {
267282
268283 // Build/test commands
269284 if ( BUILD_TEST_PATTERNS . some ( p => p . test ( cmd ) ) ) {
270- nudgeOnce ( 'bash-test' , `[tl] wrap with tl-run` , TTL_LOW ) ;
285+ nudge ( 'bash-test' , `[tl] wrap with tl-run` , TTL_LOW ) ;
271286 return ;
272287 }
273288
274289 // Tail commands
275290 if ( TAIL_PATTERNS . some ( p => p . test ( cmd ) ) ) {
276- nudgeOnce ( 'bash-tail' , `[tl] use tl-tail instead` , TTL_LOW ) ;
291+ nudge ( 'bash-tail' , `[tl] use tl-tail instead` , TTL_LOW ) ;
277292 return ;
278293 }
279294
280295 // grep/rg/ag — nudge to built-in search tool
281296 if ( / ^ \s * ( g r e p | r g | a g ) \s / . test ( cmd ) ) {
282297 const grepTool = format === 'pi' ? 'grep tool' : 'Grep tool' ;
283- nudgeOnce ( 'bash-grep' , `[tl] use ${ grepTool } , not bash` , TTL_MEDIUM ) ;
298+ nudge ( 'bash-grep' , `[tl] use ${ grepTool } , not bash` , TTL_MEDIUM ) ;
284299 return ;
285300 }
286301
287302 // head — nudge to Read with limit
288303 if ( / ^ \s * h e a d \s / . test ( cmd ) ) {
289304 const readTool = format === 'pi' ? 'read tool' : 'Read tool' ;
290- nudgeOnce ( 'bash-head' , `[tl] use ${ readTool } with offset/limit` , TTL_MEDIUM ) ;
305+ nudge ( 'bash-head' , `[tl] use ${ readTool } with offset/limit` , TTL_MEDIUM ) ;
291306 return ;
292307 }
293308
294309 // Writes via bash (heredocs, redirects) — nudge to Write tool
295310 if ( WRITE_VIA_BASH_PATTERNS . some ( p => p . test ( cmd ) ) ) {
296311 const writeTool = format === 'pi' ? 'write tool' : 'Write tool' ;
297- nudgeOnce ( 'bash-write' , `[tl] use ${ writeTool } instead of writing via bash` , TTL_MEDIUM ) ;
312+ nudge ( 'bash-write' , `[tl] use ${ writeTool } instead of writing via bash` , TTL_MEDIUM ) ;
298313 return ;
299314 }
300315
@@ -308,39 +323,39 @@ async function runHook() {
308323 if ( fp && ! fp . startsWith ( '-' ) ) {
309324 try { sizeHint = ` (${ Math . round ( statSync ( fp ) . size / 1024 ) } KB)` ; } catch { }
310325 }
311- nudgeOnce ( 'bash-cat' , `[tl] use ${ readTool } ${ sizeHint } , not cat` , TTL_HIGH ) ;
326+ nudge ( 'bash-cat' , `[tl] use ${ readTool } ${ sizeHint } , not cat` , TTL_HIGH ) ;
312327 return ;
313328 }
314329
315330 // find/fd — nudge to Glob/find tool
316331 if ( / ^ \s * ( f i n d | f d ) \s / . test ( cmd ) ) {
317332 const findTool = format === 'pi' ? 'find tool' : 'Glob tool' ;
318- nudgeOnce ( 'bash-find' , `[tl] use ${ findTool } , not bash` , TTL_MEDIUM ) ;
333+ nudge ( 'bash-find' , `[tl] use ${ findTool } , not bash` , TTL_MEDIUM ) ;
319334 return ;
320335 }
321336
322337 // ls -R / ls -la / tree — verbose directory listing
323338 if ( LS_TREE_PATTERNS . some ( p => p . test ( cmd ) ) ) {
324- nudgeOnce ( 'bash-ls-tree' , `[tl] use tl-structure or Glob for directory exploration` , TTL_MEDIUM ) ;
339+ nudge ( 'bash-ls-tree' , `[tl] use tl-structure or Glob for directory exploration` , TTL_MEDIUM ) ;
325340 return ;
326341 }
327342
328343 // git log / git diff / git show without truncation
329344 if ( GIT_VERBOSE_PATTERNS . some ( p => p . test ( cmd ) ) ) {
330- nudgeOnce ( 'bash-git-verbose' , `[tl] use tl-diff or tl-history for token-efficient git output` , TTL_MEDIUM ) ;
345+ nudge ( 'bash-git-verbose' , `[tl] use tl-diff or tl-history for token-efficient git output` , TTL_MEDIUM ) ;
331346 return ;
332347 }
333348
334349 // sed -i / awk > file — in-place edits via bash
335350 if ( SED_AWK_EDIT_PATTERNS . some ( p => p . test ( cmd ) ) ) {
336351 const editTool = format === 'pi' ? 'edit tool' : 'Edit tool' ;
337- nudgeOnce ( 'bash-sed-awk' , `[tl] use ${ editTool } for targeted edits` , TTL_MEDIUM ) ;
352+ nudge ( 'bash-sed-awk' , `[tl] use ${ editTool } for targeted edits` , TTL_MEDIUM ) ;
338353 return ;
339354 }
340355
341356 // curl on URLs (skip API calls with -X, -d, --data, -H with auth)
342357 if ( / ^ \s * c u r l \s / . test ( cmd ) && ! / ( - X \s | - - d a t a | - - h e a d e r .* a u t h | - d \s ) / i. test ( cmd ) ) {
343- nudgeOnce ( 'bash-curl' , `[tl] use tl-browse instead` , TTL_HIGH ) ;
358+ nudge ( 'bash-curl' , `[tl] use tl-browse instead` , TTL_HIGH ) ;
344359 return ;
345360 }
346361 }
@@ -349,7 +364,7 @@ async function runHook() {
349364 if ( toolName === 'WebFetch' ) {
350365 const url = toolInput . url || '' ;
351366 if ( url ) {
352- nudgeOnce ( 'webfetch' , `[tl] use tl-browse instead` , TTL_HIGH ) ;
367+ nudge ( 'webfetch' , `[tl] use tl-browse instead` , TTL_HIGH ) ;
353368 }
354369 return ;
355370 }
0 commit comments