Repository navigation
Post-processing hook: pipe finished text through a user script (#259) - #417
Merged
Merged
Conversation
A user-configured script gets the finished dictation on stdin, gets its stdout back. Kills the process and rejects on timeout (10s default); rejects on a non-zero exit or a missing/unexecutable script. Never decides what a failure MEANS for the dictation - that policy lives in dictationCoordinator.js, which is what gets unit-tested against a fake. This file is what gets tested against real child processes instead, since this session's sandbox can actually spawn plain subprocesses (confirmed separately - unlike transcription-helper, this touches no CoreML/ANE). See ADR-0004 for why this is a script path rather than an inline shell command or a second LLM-prompt hook type. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
null by default - the feature is opt-in. Not checked against the filesystem at settings-validation time; a moved or deleted script is a runtime concern for postProcessHook.js (falls back, logged), the same whether the path came from the file picker or a hand-edited settings file. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
…#259) New optional adapter, defaulting to a no-op passthrough so every existing caller and test sees unchanged behaviour. Runs after break placement, on the fully-finished text - a "held" outcome from a later delivery failure carries the hook's output too, not the pre-hook text, since by that point the hook's result is the real finished text. A throw (script failure, timeout, a non-string return) falls back to the un-hooked text rather than failing the dictation - logged via postProcess.failure, never surfaced. An empty string back from the hook is treated as no-speech rather than delivered as nothing, so a deliberate redaction filter doesn't silently type an empty result. Five pre-existing full-diagnostics-array assertions needed the new postProcess.applied entry appended - same class of update as #401's. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
postProcessHook is created at module scope, same as dictationIntake itself - its getScriptPath reads settingsStore fresh on every dictation, so a change in Settings applies to the very next utterance. IPC: settings:set-post-process-script (clears when passed null) and settings:pick-post-process-script (opens a file dialog, sets the path in one round trip - same pattern as the break-safe app picker). Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
A file-picker button when nothing's configured, a chip with the current path and a remove button once one is. The description says plainly that the script runs with full access and should only point at something the user trusts - the mitigation ADR-0004 leans on most, alongside off-by-default and a file (not a pasted command) being what gets pointed at. Claude-Session: https://claude.ai/code/session_0144sxo5CJiyk99vaR7sgjuj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #259. A post-processing hook: an optional, user-supplied script the finished text is piped through right before delivery.
Scope, decided with @Nabzx before building
Three calls made up front rather than guessed at, since this touches a real security surface:
Full reasoning, including why this is a deliberate arbitrary-code-execution feature and not an oversight, is in ADR-0004.
What's built
postProcessHook.js: spawns the script, pipes the finished text in on stdin, collects stdout, enforces a 10s timeout (kills the process on expiry). Rejects on a non-zero exit, a missing/unexecutable script, or the timeout - never decides what a failure means, just reports it.dictationCoordinator.js: the last transform before delivery, after break placement. A throw falls back to the un-hooked text (logged viapostProcess.failure, never surfaced). An empty string back from the hook is treated as no-speech rather than delivered as nothing - so a deliberate redaction filter doesn't silently type an empty result. A held delivery carries the hook's output, not the pre-hook text.postProcessScriptPath(null = off, the default), a file-picker button, a chip showing the current path with a remove button, and a plain-language warning that the script runs with full access.Verified for real, not compile-only
This session's sandbox can spawn plain subprocesses fine (confirmed separately - it's
transcription-helper's CoreML/ANE dependency that gets killed here, not process spawning generally), sopostProcessHook.test.jsruns against real executable scripts: a real uppercase transform, a real non-zero exit with stderr, a real missing-script rejection, and a real hang killed at the timeout (asserted to reject in well under the script's full sleep, proving the kill actually happened). No manual-check doc needed for the mechanism this time.Judgement call to flag
The 10s timeout isn't exposed in Settings - a user who wants a slower hook (one that calls out to a local LLM, say) is out of luck until that's revisited. Kept minimal for v1 rather than guessed at; noted in ADR-0004.
Tests
postProcessHook.test.js(8, real subprocesses),dictationCoordinator.test.js(+6, plus 5 pre-existing full-diagnostics-array assertions updated for the newpostProcess.appliedentry - same class of change as #401's),settingsStore.test.js(+1). Full suite (391) + typecheck + build green.🤖 Generated with Claude Code