feat(hooks): connect foreground hooks to the terminal#3129
feat(hooks): connect foreground hooks to the terminal#3129worktrunk-bot wants to merge 2 commits into
Conversation
Foreground (`pre-*`) hooks now inherit the parent's stdin, exactly as
aliases already do, so an interactive child keeps the controlling
terminal — a hook can prompt before continuing (e.g. `gum confirm`
before `mise trust`). Previously every foreground hook had the JSON
context piped to its stdin, which stole the tty and made interactive
hooks impossible.
The lever is the one aliases already use: `sourced_steps_to_foreground`
hard-coded `pipe_stdin = true` for hooks and `false` for aliases. With
both sides now inheriting stdin in the single-step path, the flag was
uniformly false, so the `pipe_stdin` field is removed rather than left
vestigial.
The JSON context is unchanged for the paths that can't be interactive:
concurrent hook groups and background (`post-*`) detached hooks still
receive it on stdin. Template variables (`{{ }}`) reach every hook
regardless of form.
Closes #3093
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
The core change is clean and the docs/tests match it. I traced the two paths the PR claims still receive the JSON context and both hold: concurrent groups build context_jsons and write them to each child's stdin (command_executor.rs → output/concurrent.rs), and the detached post-* pipeline writes context_json in run_pipeline.rs. The foreground single-step path (run_one_command) is the only one that flips to inheriting stdin, which is exactly the pre-* half the issue asked for. The misnamed test_post_start_script_reads_json repoint (it was configured with [pre-start] while living among the post-start tests) is a real correctness improvement — it now exercises the JSON-on-stdin path that actually survives, with wait_for_file_content for the detached write.
One thing that looks unintended: this PR adds .claude/settings.local.json containing {"permissions":{"defaultMode":"bypassPermissions",...},"skipDangerousModePermissionPrompt":true}. That's a local agent-sandbox settings file (the .local.json suffix is the convention for per-machine, gitignore'd settings), and the content is unrelated to the foreground-hooks feature. On main this file is tracked but empty; this PR fills it with bypass-permission flags. Worth dropping it from the commit (and likely adding .claude/settings.local.json to .gitignore) so the feature PR stays scoped to the hook change.
This bypass-permissions agent-sandbox settings file was committed unintentionally and is unrelated to the foreground-hooks change. Remove it from tracking and gitignore it (per the .local.json per-machine convention). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Good catch on the stray One small correction to the review note: on |
|
I'm really not sure about adding the complication around whether or not we pipe JSON in. I think we should probably either remove it completely or not merge this — any votes either way? |
|
My vote: merge as-is, with "remove JSON entirely" as the clean fallback and "don't merge" as the weakest option. Reasoning below — the key fact is that the JSON context carries no data the templates don't. The split isn't a new axis. The JSON-vs-stdin distinction this PR introduces maps exactly onto the foreground/background split that already structures hooks: If you want strict uniformity anyway, "remove it completely" is strictly better than "don't merge" — because the JSON context loses nothing. It's built from the same So my ranking: merge as-is ≥ remove JSON entirely ≫ don't merge. Caveats if you go the "remove entirely" route
|
Problem
Foreground hooks couldn't run interactively. Every foreground hook had the JSON context piped to its stdin, which stole the controlling terminal, so a hook like
gum confirm 'trust this worktree?' && mise trustcould never see a TTY. The issue asked for a way to gate a hook behind a confirmation — #3093.Solution
Per the maintainer's call in the thread ("implement the same as we have with aliases, so a simple foreground hook gets connected to stdin"), foreground (
pre-*) hooks now inherit the parent's stdin exactly as aliases already do. A foreground hook keeps the controlling terminal and can prompt before continuing:The lever is the one aliases already used:
sourced_steps_to_foregroundhard-codedpipe_stdin = truefor hooks andfalsefor aliases. With both sides now inheriting stdin in the single-step foreground path, the flag was uniformlyfalse— so rather than leave it as a vestigial always-false field, thepipe_stdinfield is removed and the single-step path simply never pipes.The trade-off the maintainer flagged — giving up the JSON-on-stdin context — applies only to foreground hooks. The JSON context is unchanged for the paths that can't be interactive anyway: concurrent hook groups and background (
post-*) detached hooks still receive it on stdin. Template variables ({{ branch }}, etc.) reach every hook regardless of form.Scope is deliberately the
pre-*half only; promotingpost-*hooks out of the detached path is the separate request tracked in #3102.Testing
test_pre_start_json_stdin→test_pre_start_inherits_stdin: pipes a sentinel towt switch's stdin and asserts apre-starthook captures the raw bytes verbatim (and that the JSON context is not piped).test_post_start_script_reads_jsonwas misnamed — it configured a[pre-start]hook despite living among the post-start tests. Repointed it at[post-start](withwait_for_file_contentfor the detached write) so it now genuinely covers the JSON-on-stdin path that survives.test_post_start_json_stdin(unchanged) continues to assert background hooks receive the JSON context.post_start_commands,user_hooks,step_alias,merge, andswitchsuites pass locally; docs regenerated viatest_docs_are_in_sync. One unrelated failure (test_switch_pr_self_hosted_tea_authed_dispatches_to_gitea) is an environmental snapshot mismatch in the CI sandbox — the HTTP proxy returns502 CONNECT tunnel failedwhere the snapshot expectsCould not resolve host; it touches no hook code.Docs
Updated the hook docs (
## Interactive hooks and JSON context) and the hooks-vs-aliases stdin row inextending.mdto describe the new split:pre-*inherit the terminal,post-*receive JSON.Closes #3093