Open-source project
cortesi/modd avatar
cortesi/modd

cortesi/modd: a file-watcher that runs prep commands and keeps daemons alive

A flexible developer tool that runs processes and responds to filesystem changes

2,967 stars136 forksGoMIT

At a glance

What is it?
Modd watches a directory tree, batches filesystem changes, then runs one-shot prep commands and restarts long-running daemons. It is a small Go binary aimed at developers replacing ad-hoc Gulp or Grunt pipelines with a portable modd.conf.
Who is it for?
Adopt modd if you already have a shell command that rebuilds or retests your project and you want it to run on file changes without a Node or Python toolchain. Skip it if you need cross-platform symlink traversal, a GUI, or a plugin ecosystem; the README says modd does not implicitly traverse symlinks, and the documented flags are command-line only.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 102 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What cortesi/modd replaces in a development loop

Modd is a developer tool that triggers commands and manages daemons in response to filesystem changes. That sentence from the README is the whole scope. It is not a build system, not a task runner with dependency graphs, and not a container orchestrator. It is a single process that watches a tree, decides which blocks of a config file are affected, and executes shell commands.

The intended user is someone who already knows the command they want to run. A Go developer who wants `go test` after every edit. A front-end developer who wants a rebuild and a browser reload. The README points at an examples directory with frontend.conf (React + Browserify + Babel), go.conf (live unit tests for Go) and python.conf (Python + Redis with devd managing livereload). The frontend example is described as replacing many functions of Gulp and Grunt, which is the clearest statement of intent in the repository: modd is for people who want the watcher, not the plugin ecosystem.

Because the config is meant to be portable and safe to check into source repositories, the design pushes personal preferences out of the file and into command-line flags. The README gives desktop notifications as the example of something controlled by flags rather than config. That is a deliberate trade-off: your team shares one modd.conf, but each developer can still decide how they want to be interrupted.

Prep commands, daemons and the batching that holds them together

A modd.conf is one or more blocks. Each block pairs a set of file patterns with commands. Commands come in two flavors. Prep commands run and terminate, covering compilation, test suites and linters. Daemon commands run and keep running, covering databases and webservers.

The ordering rules are strict and worth reading twice. Prep commands run in order of occurrence. If any prep command exits with an error, execution of the current block stops immediately. Only if all prep commands succeed are the daemons in that block restarted, also in order. If several blocks are triggered by the same set of changes, they run top to bottom. This means a failing test prevents a restart, which is usually what you want and occasionally is not.

Daemons are sent a SIGHUP by default when their block is triggered, and are restarted if they ever exit. The signal is configurable per command with a flag such as +sigterm. The README's devd example explains why this matters: devd treats SIGHUP as a request to trigger a browser livereload, but when developing devd itself you want the process to exit and restart, so the config sends SIGTERM instead.

On the watching side, modd batches changes until there is a lull in filesystem activity. Coherent processes that touch many files, such as compilation or rendering, are therefore likely to trigger commands only once. Patterns match on a batch, and a block fires when the first match in that batch is seen. Paths are always slash-delimited, even on Windows, and are cleaned and normalised before matching. Two variables carry the results: @mods expands to the changed files, and @dirmods expands to a properly escaped list of directories containing changed files. On first run, @dirmods includes all directories containing matching files, which is how the quick-start config runs every Go module's tests at startup and then only the affected module afterwards.

Installing modd and running a first modd.conf

The README describes two installation routes. The first is to download a package for your OS from the releases page and copy the binary somewhere on your PATH. Releases are listed for OSX, Windows, Linux, FreeBSD, NetBSD and OpenBSD, and the README describes modd as a single binary with no external dependencies.

The second route needs Go 1.17 or newer. The README notes that CGO is required, so if it is disabled you must prepend the environment variable:

bash
CGO_ENABLED=1 go install github.com/cortesi/modd/cmd/modd@latest

After that, create a file named modd.conf in the directory you want to watch. The quick start in the README is two lines:

code
**/*.go {
    prep: go test @dirmods
}

Running modd with no arguments in that directory starts the watcher:

bash
modd

The README states what you should see: the first time modd runs it tests all Go modules, and whenever a .go file is modified it runs go test only on the enclosing module. If you want to keep a process alive as well, the README's devd example adds a second block that excludes test files and launches a daemon:

code
**/*.go !**/*_test.go {
    prep: go install ./cmd/devd
    daemon +sigterm: devd -m ./tmp
}

The negation is written outside the quotes for quoted patterns, and negations are applied after all positive patterns: modd collects everything matching the positives, then removes the negated files. If you want to see what is being ignored, the -i flag lists the default ignore patterns, and the special +noignore flag disables them for a block.

Where modd's pattern language and symlink handling bite

The pattern rules have sharp edges, and the README documents them rather than hiding them. The first is the leading ./ problem. Patterns and paths are normalised, and if a path is inside the working directory it becomes relative, so a pattern like ./*.js will never match. You write *.js instead. This is a small thing that will cost a newcomer an afternoon.

The second is symlinks. Modd does not implicitly traverse symlinks. To monitor one you split the path specification from the matching pattern, so the directory part names the symlink and the pattern applies inside it. The README explains that modd resolves the symlinked directory as if it had been specified directly, and the text is cut off just as it starts discussing what happens when the symlink destination lies outside the working directory. That is a real gap in the documentation for anyone using a symlinked checkout or a linked node_modules directory.

The third is daemon supervision. Daemons are restarted if they exit, which is convenient until it is not. A daemon that crashes on startup because of a configuration typo will be restarted repeatedly. There is no documented backoff, retry limit or circuit breaker in the README, so a fast-crashing daemon is a case where you should watch the terminal rather than leave modd running unattended.

Finally, the empty match pattern is a special case. A block with no pattern runs prep commands once at startup, and daemons are restarted if they exit, but they are never explicitly signalled to restart by modd. That is a useful way to express a one-shot setup step, and a confusing one if you expected the block to fire on changes.

How modd differs from entr, watchexec and devd

The closest comparison is a general-purpose command watcher such as entr or watchexec. Those tools take a list of files on standard input or a path argument and run one command when something changes. Modd instead owns a config file with multiple blocks, distinguishes prep commands from daemons, and keeps the daemons under supervision. If your need is "run this one command when this one file changes", a watcher is less machinery. If your need is "compile, test, then restart the server, and do not restart the server if the tests fail", modd's block semantics are doing work that a plain watcher does not.

The other comparison the README itself makes is devd, a compact HTTP daemon for developers from the same author. Devd is not an alternative to modd; the README says devd integrates with modd, allowing you to trigger in-browser livereload with modd. The division is that devd serves your files and modd decides when to signal it. The devd example in the README is instructive precisely because it shows the two tools disagreeing about SIGHUP: devd uses SIGHUP for livereload, so modd is told to send SIGTERM when the goal is a restart.

Against Gulp or Grunt, the difference is not features but surface area. Those tools bring a package manager, a plugin registry and a JavaScript build file. Modd brings a binary and a config with file patterns and shell commands. The frontend example exists to show that the modd approach covers a React + Browserify + Babel pipeline, but anyone who depends on a specific Gulp plugin will not find an equivalent here.

Maintenance, licence and the cost of upgrading

The repository is not archived. The last push was on 2026-06-21, which is recent enough that the project is being touched, though the release list tells a different story about tagging: v0.8 dates from 2019-01-20, and the releases before it are v0.7 from 2018-07-25 and v0.6 from 2018-04-12. So the code moves while the version number has not. If you pin to a release tarball you are pinning to something from 2019, and if you build from master you are tracking an untagged tree.

The go.mod file gives the dependency picture. The module declares go 1.25.0 and depends on github.com/cortesi/moddwatch, github.com/cortesi/termlog, github.com/google/go-cmp, gopkg.in/alecthomas/kingpin.v2 and mvdan.cc/sh/v3. The shell dependency is the interesting one: modd interprets commands with a built-in POSIX-like shell, and mvdan.cc/sh is that shell. The README notes that some external shells are supported and can be selected by setting the @shell variable in modd.conf, so scripts that rely on bash-specific syntax have an escape hatch, but the default is the built-in interpreter and you should expect to test anything unusual.

Modd is MIT licensed, which is permissive and imposes no copyleft obligation on your config or your project. That is a statement about the licence text, not legal advice; check the LICENSE file in the repository if the distinction matters to your organisation. Upgrade cost is mostly about the config format, and the README presents modd.conf as portable and safe to commit, which suggests the format is intended to be stable. The absence of releases since 2019 means there is no changelog entry to read for behaviour changes between v0.8 and the current tree; CHANGELOG.md exists at the top level if you want to check what it records.

Editorial conclusion

Adopt modd if you already have a shell command that rebuilds or retests your project and you want it to run on file changes without a Node or Python toolchain. Skip it if you need cross-platform symlink traversal, a GUI, or a plugin ecosystem; the README says modd does not implicitly traverse symlinks, and the documented flags are command-line only. Before committing, verify that your modd.conf patterns match the paths modd actually sees, since the README warns that a leading ./ in a pattern will never match, and check whether your daemon should receive SIGHUP or SIGTERM.

Frequently asked questions

How do I install modd?

Download the package for your OS from the releases page and copy the binary to somewhere on your PATH, or with Go 1.17 or newer run go install github.com/cortesi/modd/cmd/modd@latest. The README notes CGO is required, so prepend CGO_ENABLED=1 if it is disabled.

What is the difference between a prep command and a daemon in modd?

Prep commands run and terminate, such as compiling or running tests, and if one exits with an error the rest of the block stops. Daemons run and keep running, are sent SIGHUP by default when their block is triggered, and are restarted if they exit.

Why does my modd file pattern not match anything?

The README warns that paths are normalised and made relative to the working directory, so a pattern like ./*.js never matches because inbound paths have no leading ./ component. Use *.js instead.

Official sources

  1. cortesi/modd on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cortesi-modd.svg)](https://hysenlabs.com/projects/cortesi-modd)