All notable changes to Machook are documented here. The format follows Keep a Changelog, and versions follow Semantic Versioning.
- Async endpoints no longer have a timeout. The wall-clock limit exists
to stop a synchronous caller hanging, and an async caller already holds
its
201. Applying the endpoint'sTimeoutanyway killed exactly the long jobs async mode was turned on for: the log recorded a run the caller was told was accepted, then a second line 30 seconds later marking it504and SIGTERM'd. Async runs are still bounded by Max concurrent commands and by the output cap. TheTimeoutfield still applies to synchronous endpoints and to the editor's Test button. - Execution log records an
outcome. An async run's HTTP status is always201, because that is what the caller was told, sostatusCodecould not distinguish an accepted run from a finished, failed, or killed one. Records now carryoutcome(accepted,succeeded,failed,timed out,rejected), and the menu bar and Settings key their glyph off it. Older log lines without the field are decoded by inferring it, so an existing log still reads back.
- The endpoint editor's Timeout help text claimed the limit applied to async endpoints while saying it was ignored. It now states plainly that it has no effect while Async is on.
- Async endpoints. Each endpoint now has an Async toggle. When enabled,
the HTTP caller receives 201 Created with a
run_idimmediately, and the command keeps running in the background. Async endpoints still respect timeout, output cap, and concurrency limits; they just don't make the caller wait. MCP tool calls also honor the flag and return an immediate accepted result. - Persistent execution log. Every run is appended to
~/Library/Logs/machook/executions.jsonlas a JSON line containing stdout, stderr, exit code, duration, status code, and anasyncflag. The in-memory Recent runs ring is now hydrated from this store on launch, so history survives relaunch and async completions remain inspectable. The log rotates at 10 MB. - Log viewer. Settings → General → Recent runs now shows an async tag and an Open log button that reveals the log folder in Finder.
First release. A macOS menu bar app that runs local shell commands in response to incoming webhooks.
- Endpoint table. Map a request path to a shell command, edited in Settings: methods, timeout, working directory, enable/disable. Endpoints resolve against the live configuration on every request, so changes take effect with no restart.
- Request envelope. Every request is written to a private temp file
(
0600in a0700per-user directory) as JSON — method, path, query, headers, parsed body, raw body — and the file's path replaces{{request}}in the command. TheAuthorizationheader is stripped before the file is written, the file is deleted when the command exits, and stale files are swept at launch. The path is also exported asMACHOOK_REQUEST_FILE. - Synchronous responses. stdout becomes the response body with a sniffed
content type, the exit code becomes the status:
200on success,500with stderr on a non-zero exit,504on timeout,503at the concurrency cap or for a disabled endpoint,405for a rejected verb,404for an unknown path. Runs reportX-Machook-Exit-Code,X-Machook-Duration-Ms, andX-Machook-Truncated. - MCP server.
POST /mcppublishes each endpoint as a tool, with the catalog generated from the live endpoint table. Tool calls execute the same command against the same envelope shape, with tool arguments delivered as the body. Per-endpoint tool name, description, input schema, and read-only annotation. - Cloudflare Tunnel. Bundled and supervised
cloudflaredin quick mode (ephemeraltrycloudflare.comURL, no account) or named mode (stable hostname). The current URL is in the menu bar; click to copy. - Verified tunnel URLs. A published URL is not a working one: a quick tunnel
is handed a hostname before its DNS record exists, and the gap can be
minutes while every other signal looks healthy. Machook fetches
<url>/healththrough the public URL — a Fibonacci warm-up over ~90 seconds, then every minute, then every five for as long as the tunnel is up — and reports the outcome in the menu, in Settings, and astunnel_reachabilityonGET /status. A record that appears late flips the state to reachable on its own instead of leaving a stale failure on screen. Failures are named rather than generic: an edge with no connector (Cloudflare error 1033), a tunnel that reaches us but finds nothing listening, an Access policy in the way. A DNS failure is cross-checked against a public resolver over DNS-over-HTTPS, so "Cloudflare has not published the record" is distinguished from "the record exists and this Mac cannot see it" — usually macOS caching the failures from before the record appeared, which needssudo dscacheutil -flushcacherather than a new tunnel. That cross-check runs before each probe, not just after a failure: asking the system resolver for a name that does not exist yet makes macOS cache theNXDOMAINfor the whole negative TTL, which broke the user's owncurlto a tunnel that had come up fine. - Tunnel process hygiene.
SIGTERMandSIGINTnow quit the app through the normal path, sopkillno longer leaves a running menu bar app whose listener is dead. Acloudflaredorphaned by a crash or a force-quit is terminated at the next launch, matched only by the exact executable path inside our own bundle so unrelated tunnels on the machine are never touched. If the listener stops without being asked to, the menu says so instead of looking healthy. - Readable tunnel logs.
cloudflaredoutput was recorded at debug level, which a defaultlog showdrops — the failure was invisible in exactly the case you would go looking for it. Errors, warnings, and connection loss are promoted toerrorornotice. The connector token is passed through the environment rather than--token, so it no longer appears inpsoutput. - Bearer token auth on every route except
GET /health, compared in constant time. Settings refuses to save an enabled tunnel with an empty token. - Shell injection boundary. The command template from Settings is the only string the shell parses; network data arrives as a quoted argv element or a file path. Quoting only passes a value through unquoted when every character is provably inert.
- Command runner with a wall-clock timeout (SIGTERM, then SIGKILL two seconds
later), a per-stream output cap that keeps draining past the cap so the child
never blocks on a full pipe, a concurrency budget,
/dev/nullon stdin, and an optional login shell so Homebrew and pyenv paths resolve. - Port fallback. The listener tries
localAPIPort(7876) and thenfallbackAPIPort(7877), since a fixed default port is a coin flip on a machine that already runs something there. It only moves before it has served traffic, the menu bar and Settings say when the fallback is in use, and the tunnel is started against the port that actually bound rather than the configured one. Set the fallback to0to insist on a single port. - Menu bar surface: tunnel URL, endpoint and tool counts, the last few runs, a note when the fallback port is in use, and a warning line when the HTTP listener cannot bind at all.
- Built-in routes:
GET /health(unauthenticated) andGET /status. - Sample endpoint.
examples/log-request.shrecords each request to~/Library/Logs/machook/requests.log(0600, in a 0700 directory, rotated at 5 MiB) and answers with a JSON summary, so a new install can be exercised end to end before any real command is wired up.?fail=1exits non-zero on purpose to demonstrate the 500-plus-stderr mapping. - Sparkle auto-updates, Developer ID signing, and notarization in CI.
- Universal builds. Releases ship one
machook-universalartifact whose executable, bundledcloudflared, andSparkle.frameworkeach carry botharm64andx86_64. A Sparkle appcast channel advertises a single<enclosure>with no architecture filtering, so a per-arch release would offer half the user base a build their Mac cannot launch; CI now fails if any shipped binary is missing a slice. The appcast'sminimumSystemVersionis read fromLSMinimumSystemVersionrather than hardcoded, so an update is never offered to a macOS version the app cannot run on. - 79 unit tests covering shell quoting, template rendering, path and tool-name normalization, endpoint validation, configuration decoding, envelope shape and file permissions, port-candidate selection, the port fallback against a real occupied socket, reachability classification, cloudflared log levels, stray process matching (including that another app's tunnel is never a candidate), DNS cross-check parsing and the verdicts derived from it, restart generation bookkeeping, listener shutdown reporting, and runner behaviour including timeouts, output caps, concurrency, and that injected metacharacters do not execute.