Liking cljdoc? Tell your friends :D

koine.process

Subprocesses, portable.

sh runs a command to completion; spawn keeps a long-lived child with piped stdin/stdout, which is what an MCP stdio transport needs and what sh cannot express.

Both have an OFF SWITCH: sh takes :timeout-ms, and a spawned child takes kill!. A subprocess is the most dangerous thing most programs do, and one that cannot be stopped is a program that cannot be stopped.

Subprocesses, portable.

`sh` runs a command to completion; `spawn` keeps a long-lived child with piped
stdin/stdout, which is what an MCP stdio transport needs and what `sh` cannot
express.

Both have an OFF SWITCH: `sh` takes `:timeout-ms`, and a spawned child takes
`kill!`. A subprocess is the most dangerous thing most programs do, and one
that cannot be stopped is a program that cannot be stopped.
raw docstring

alive?clj/s

(alive? child)

True while the child is running.

True while the child is running.
sourceraw docstring

close!clj/s

(close! child)

Close the child's stdin, wait for it to exit, and return the exit code.

This is the POLITE shutdown and it WAITS: a child that ignores its stdin closing will hang here forever. When you need a guarantee rather than a request, use kill!.

Close the child's stdin, wait for it to exit, and return the exit code.

This is the POLITE shutdown and it WAITS: a child that ignores its stdin
closing will hang here forever. When you need a guarantee rather than a
request, use `kill!`.
sourceraw docstring

kill!clj/s

(kill! child)

Force-terminate the child. Returns nil — always, on both hosts.

Returns nil rather than an exit code on purpose: a killed process did not choose one. The JVM would report 137 and Go -1, and koine does not invent agreement between two host-specific numbers. Ask alive? if you need to know it is gone.

This is also the way OUT of a blocked read-line!. A reader parked on a hung peer cannot be interrupted portably, but killing the child closes its stdout, so the parked read-line! hits EOF and returns nil. One mechanism ends a runaway command and frees a stuck transport — asked for by the toolnexus port as two separate things, 2026-07-31.

Force-terminate the child. Returns nil — always, on both hosts.

Returns nil rather than an exit code on purpose: a killed process did not
choose one. The JVM would report 137 and Go -1, and koine does not invent
agreement between two host-specific numbers. Ask `alive?` if you need to know
it is gone.

This is also the way OUT of a blocked `read-line!`. A reader parked on a hung
peer cannot be interrupted portably, but killing the child closes its stdout,
so the parked `read-line!` hits EOF and returns nil. One mechanism ends a
runaway command and frees a stuck transport — asked for by the toolnexus port
as two separate things, 2026-07-31.
sourceraw docstring

read-line!clj/s

(read-line! child)

Block for one line from the child's stdout, WITHOUT the terminator. nil at EOF.

Block for one line from the child's stdout, WITHOUT the terminator.
nil at EOF.
sourceraw docstring

run-async!clj/s

(run-async! f)

Run f on a background thread that must NOT keep the program alive.

NOT future on the JVM. Clojure's future pool threads are non-daemon with a 60-second keep-alive, so a program that called ONE koine.process fn sat there for a full minute after printing its answer — no output, no CPU, nothing to see. (shutdown-agents) fixes it, but a library cannot demand that of a consumer, and a library must never decide when the host program may exit. Measured 2026-07-31: 60.5s for a sh ["true"].

On cljgo the equivalent is a goroutine, which never holds up exit.

PUBLIC, and that is the point. This was private while koine used it to fix its own 60-second hang, which quietly handed the same hang to every consumer: a caller running its own reader loop over read-line! reaches for future, gets the non-daemon pool, and cannot call (shutdown-agents) from library code either. Measured by the toolnexus port on their MCP suite — 64.9s with future, 4.3s with the pool shut down, cljgo 4.5s either way. Fixing that only inside koine was fixing it for koine, not for anyone using koine.

Returns immediately. f takes no arguments; its value is discarded, so communicate through an atom or a promise. Nothing waits for it — if you need to know it finished, deliver a promise at the end of f.

Run `f` on a background thread that must NOT keep the program alive.

NOT `future` on the JVM. Clojure's future pool threads are non-daemon with a
60-second keep-alive, so a program that called ONE koine.process fn sat there
for a full minute after printing its answer — no output, no CPU, nothing to
see. `(shutdown-agents)` fixes it, but a library cannot demand that of a
consumer, and a library must never decide when the host program may exit.
Measured 2026-07-31: 60.5s for a `sh ["true"]`.

On cljgo the equivalent is a goroutine, which never holds up exit.

PUBLIC, and that is the point. This was private while koine used it to fix its
own 60-second hang, which quietly handed the same hang to every consumer: a
caller running its own reader loop over `read-line!` reaches for `future`,
gets the non-daemon pool, and cannot call `(shutdown-agents)` from library
code either. Measured by the toolnexus port on their MCP suite — 64.9s with
`future`, 4.3s with the pool shut down, cljgo 4.5s either way. Fixing that
only inside koine was fixing it for koine, not for anyone using koine.

Returns immediately. `f` takes no arguments; its value is discarded, so
communicate through an atom or a promise. Nothing waits for it — if you need
to know it finished, deliver a promise at the end of `f`.
sourceraw docstring

send-line!clj/s

(send-line! child s)

Write s + newline to the child's stdin and flush. Returns nil.

Write `s` + newline to the child's stdin and flush. Returns nil.
sourceraw docstring

shclj/s

(sh command)
(sh command {:keys [in dir env timeout-ms]})

Run command (a vector) to completion. Returns {:out :err :exit :timed-out?}. Never throws on a non-zero exit — that is a normal result.

opts: :in (string on stdin) :dir :env :timeout-ms

:timeout-ms force-kills the command after n milliseconds. A killed command reports :timed-out? true and :exit nil — NOT a number. There is no exit code when a process is killed: the JVM would report 137 and Go -1, both of them inventions, and a caller doing (zero? (:exit r)) must not silently read a kill as a clean run. :timed-out? is always present, so it is safe to test on every result. :out / :err carry whatever the command managed to write before it died, which is best-effort and NOT guaranteed to match across hosts.

Without :timeout-ms the command runs to completion, however long that is. Asked for by the toolnexus port: an agent's bash tool needs an off switch.

Run `command` (a vector) to completion. Returns
{:out :err :exit :timed-out?}. Never throws on a non-zero exit — that is a
normal result.

opts: :in (string on stdin) :dir :env :timeout-ms

`:timeout-ms` force-kills the command after n milliseconds. A killed command
reports `:timed-out? true` and `:exit nil` — NOT a number. There is no exit
code when a process is killed: the JVM would report 137 and Go -1, both of
them inventions, and a caller doing `(zero? (:exit r))` must not silently read
a kill as a clean run. `:timed-out?` is always present, so it is safe to test
on every result. `:out` / `:err` carry whatever the command managed to write
before it died, which is best-effort and NOT guaranteed to match across hosts.

Without `:timeout-ms` the command runs to completion, however long that is.
Asked for by the toolnexus port: an agent's `bash` tool needs an off switch.
sourceraw docstring

spawnclj/s

(spawn command)
(spawn command {:keys [dir env]})

Start command (a vector) as a LONG-LIVED child with piped stdin/stdout and return a child handle — a map of closures, see above. This is what a line-delimited JSON-RPC transport (MCP stdio) requires; sh cannot express it.

The child's STDERR is drained from the moment it starts — see the comment above; not draining it deadlocks the child once the pipe buffer fills. Read it with stderr-lines.

opts: :dir :env

Start `command` (a vector) as a LONG-LIVED child with piped stdin/stdout and
return a child handle — a map of closures, see above. This is what a
line-delimited JSON-RPC transport (MCP stdio) requires; `sh` cannot express it.

The child's STDERR is drained from the moment it starts — see the comment
above; not draining it deadlocks the child once the pipe buffer fills. Read it
with `stderr-lines`.

opts: :dir :env
sourceraw docstring

stderr-linesclj/s

(stderr-lines child)

The child's most recent stderr lines (up to 200), oldest first, as a vector.

Always available — stderr is drained from the moment the child starts, so this is a snapshot of a buffer that is being filled for you, not a read that could block. Returns [] when the child has written nothing.

The child's most recent stderr lines (up to 200), oldest first, as a vector.

Always available — stderr is drained from the moment the child starts, so this
is a snapshot of a buffer that is being filled for you, not a read that could
block. Returns [] when the child has written nothing.
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close