A runner for your server binaries.
- Restarts on builds — watches the compiled binary, not your source. Recompiling is your editor's or your agent's job; makedog notices the new build and bounces the process.
- Logs every run — each execution is recorded to a durable store: replay it, tail it, diff two runs, grep the last week.
- Live everywhere — other shells and agents watch in real time:
tailfollows output across restarts,currentgates on the run of the build on disk,nextcatches the coming run from its first line,--followstreams one in flight. - Sends signals — from an interactive menu, or remotely from another terminal.
- Runs make targets — pick a Makefile target from within the watch session.
- Remote control — every instance listens on a unix socket:
status,restart,stop,start,quit,signal,loglevelfrom another terminal. - Agent-friendly —
--jsonoutput, and readiness gates likecurrent --until 'listening' --timeout 30swith meaningful exit codes.
Supervise a server; rebuilds restart it automatically:
$ makedog bin/server
keys: 'c' to clear screen, 'h' for this help, 'k' to mark log, 'l' to set log level, 'm' to run make, 'q' to quit, 's' to send signal, 't' to run make targets, 'x' to start/stop
--> start 1 bin/server (pid 30934, hash 7fbd0d4)
[2026-07-23 16:07:35.722] listening on :8080
[2026-07-23 16:07:36.723] GET /healthz 200 0.4ms
[2026-07-23 16:07:37.724] GET /api/items 200 12.1ms
[2026-07-23 16:12:27.493] BLE central state changed: PoweredOn
[2026-07-23 16:12:27.493] [SensorManager] BLE ready, loading sensors...
[2026-07-23 16:12:27.496] [SensorManager] Loaded 0 active sensors
[2026-07-23 16:12:27.496] Starting BLE scan (service: FEBC)
--> stop 1 bin/server [4MB memory, 2s cpu time, 5m wall time]
* binary modified at 16:12:39.541
--> start 2 bin/server (pid 30965, hash a80f21e)
[2026-07-23 16:12:40.146] listening on :8080While it runs, single keys drive it: r restart, x stop/start, s signal menu, t make-target menu, l log level menu, k mark the log, q quit.
From another terminal, read back what's happened or happening. Results are real time.
$ makedog runs # list recorded runs
$ makedog latest # replay the latest run (possibly live)
$ makedog latest~1 --plain # the run before, ANSI stripped
$ makedog current # the run of the build on disk, waiting if needed
$ makedog diff # what changed between the last two runs?
$ makedog crash # the runs where we detected the binary spinning
$ makedog search 'ERROR|panic' --since 2dGate on readiness. After a rebuild, wait for the run of the binary now on disk to say it's up.
$ make && makedog current --until 'listening on' --timeout 30sExit 0 when the line matches, 1 if the run ends first, 2 on timeout. current keys on the binary's hash, so it works called at any point after the build: it returns at once when that run is already up (even if a no-op build restarted nothing), and waits for the restart otherwise. (next waits for whatever run starts next.)
Poke the live process from another terminal:
$ makedog status
$ makedog restart
$ makedog signal HUPSee the manpage for more details.
$ brew install jeremywohl/tap/makedogOr build from source (requires Go):
$ git clone https://github.com/jeremywohl/makedog && cd makedog && makeThe binary lands in bin/makedog.
Paste this into your CLAUDE.md / AGENTS.md and your agent can rebuild, confirm the restart, and read the logs, without holding the server process itself:
## Running the server
The server runs under makedog, which restarts it whenever the binary
is rebuilt. Every run's output and status are logged and readable in
real time; never start, stop, or kill the server process yourself.
Example invocations:
- Print output so far from the run of the build on disk,
waiting for it to start if needed: `makedog current`, often paired
with options `--until '<regex to stop reading at>'` (implies --follow),
and `--timeout 30s` (exit 0 = matched, 1 = run ended first, 2 = timeout).
Run any time after builds, e.g. `make && makedog current <opts>`.
- Check the current run: `makedog info --json`, showing pid, binary
hash, git state, start time, live or exited. This answers "is it
up now?"; `current` (with --until) answers "tell me when this
build is up."
- Print the last/live run immediately: `makedog latest`.
- Other options for current/latest/next: `--follow` streams until end of run,
`-n 100` and `--since 5m` trim, `--json` emits JSONL records, `--plain`
strips ANSI.
- Dig through history: `makedog search '<regex>' --since 2d`; compare
two runs with `makedog diff`.
- After a crash loop (makedog pauses once the binary exits repeatedly on
start): `makedog crash` prints those runs back to back, exit first.
- If the project defines log levels (`makedog loglevel --list`), raise
verbosity for one run with `makedog loglevel debug --restart`; it
reverts on the start after that.
- Human-driven, rarely for agents (rebuilds already restart the server):
`makedog restart | stop | start | status`. `makedog signal <SIG>`
delivers whatever the app wired that signal to — know the handler
before sending.
- See `makedog --help` for the overview, and `makedog <verb> --help` for a
verb's options, exit codes, and examples.Every execution of your binary is a run. Every line of output and every lifecycle event is recorded, and can be read back:
| verb | does |
|---|---|
runs |
list recorded runs (--long for full metadata cards) |
<run#>, latest[~N] |
replay a run (--follow to stream a live one) |
current |
the run of the binary as built on disk, waiting for it if needed |
next |
wait for the next run and stream it from its start |
tail |
follow live output across restarts, endlessly |
search <regex> |
grep recorded runs; --runs 30..34, --since 6h, --all-binaries |
info [ref] |
one run's metadata card |
diff [a [b]] |
unified diff of two runs' output |
crash |
the runs that failed on start, causing makedog to pause |
path [ref] |
the log's file path |
These all take --json for raw records, --plain to strip ANSI, --binary to pick among a project's binaries, and -C dir to reach another project.
Runs are recorded as JSONL: one record per output line, plus lifecycle events (starts, stops, signals, marks, make invocations). Kept under ~/.local/state/makedog (override with MAKEDOG_STATE_DIR).
Older logs are compressed in the background (zstd, after 24h idle) and pruned by count, age, or total size per the retention policy. The newest two runs are always left untouched.
s opens a signal menu with the common set. Config can scope and name each in your project:
[[signals]]
signal = "HUP"
name = "reload config"
[[signals]]
signal = "USR1"
name = "rotate logs"Configured signals replace the default menu — your project's menu shows exactly these. The remote verb is unconstrained: makedog signal USR2 sends anything makedog knows.
l opens a level menu in the watch session; makedog loglevel <level> does the same from any terminal (--list shows what's defined). Makedog defines no levels itself, config maps each word to an action:
[loglevel]
env = "DEBUG"
debug.env = "app:*,http:*" # restart with DEBUG=app:*,http:*
info.env = "app:*" # restart with DEBUG=app:*
up.signal = "USR1" # send the process USR1
mute.command = "curl -sX PUT localhost:8080/log/level -d off"This defines all the valid levels, and you can mix mechanisms: env, signal, or command.
An env entry restarts the binary with the assignment in its environment. (The verb asks to restart first, or --restart skips.)
signal and command entries act on the live run.
Alternatively, a bare env = "LOG_LEVEL", or command = "curl ..%s..", with no named levels passes any word straight through: makedog loglevel trace becomes LOG_LEVEL=trace. Whereas naming levels closes the vocabulary; unknown words then error.
Note: level changes are temporary, the next run reverts to baseline.
Each watching instance registers on a unix socket, so from another terminal in the project directory:
$ makedog status # who's live?
$ makedog stop # park the binary (makedog keeps watching)
$ makedog start
$ makedog restart
$ makedog signal TERM
$ makedog loglevel debug --restart
$ makedog quit # stop the binary and exit makedog itselfWith several instances live, mutating verbs ask for --instance <pid> rather than guessing. From any other directory, -C <dir> points the verb at the project.
makedog loads .makedog.toml from the project directory (or --config path). Missing is fine; malformed warns and runs with defaults.
[[signals]] # scope and name the signal menu
signal = "HUP"
name = "reload config"
[loglevel] # dynamic log levels (see Log levels above)
env = "DEBUG"
debug.env = "app:*,http:*"
up.signal = "USR1"
[logs] # retention; zero disables a rule
compress_after = "24h" # default
keep_runs = 500 # default
max_age = "90d" # default: never expire
max_total = "512MB" # default: unlimited