Start
Each command runs in its own process session, with the port it was given in PORT and a small, deliberate environment.
v2.2.0 ยท macOS & Linux
grat = grat runs approved tasks
grat replaces the terminal tabs used to run a local development stack. Declare the commands for a frontend, an API, a dashboard or a background worker once, then manage them together. When a webhook has to reach your machine, grat publishes a single path to the internet.
Starting services example.com SERVICE STATE ENDPOINT frontend โ Ready http://localhost:3000/ backend โ Ready http://localhost:4001/ worker โ Ready 3 of 3 services complete
Release binaries are published for macOS and Linux on Intel and ARM.
grat help prints a short overview of every command there is. For a full description, the package ships a complete set of man pages. man grat, or man 1 grat, describes every command along with how grat works. man grat.config, or man 7 grat.config, describes every option the configuration file takes.
grat.config from what it finds. Give it a path and it searches below there, so a whole folder of projects is set up in one pass.One project can start a React frontend, a Laravel or Vapor API, and a queue worker with one command. grat is tied to no language, framework or package manager: it starts your command through /bin/sh from the project root, in a session of its own, and hands it the port in PORT. Whether that command is Node, PHP, Python, Swift or Go makes no difference to it.
| Stack | The command in grat.config |
|---|---|
| React with Vite | npx vite dev --port $PORT --host 127.0.0.1 --strictPort |
| Next.js | npx next dev --port $PORT --hostname 127.0.0.1 |
| Laravel | php artisan serve --host=127.0.0.1 --port=$PORT |
| Rails | bin/rails server -b 127.0.0.1 -p $PORT |
| Django | python manage.py runserver 127.0.0.1:$PORT |
| Swift with Vapor | swift run App serve --hostname 127.0.0.1 --port $PORT |
| Python with FastAPI | uvicorn main:app --host 127.0.0.1 --port $PORT --reload |
| Go | go run ./cmd/server |
| Queue worker | php artisan queue:work |
Two kinds of command fit, and both stay in the foreground. An HTTP service takes a configurable local port and answers its health path with a status from 200 through 299. A worker has no port at all and only has to stay alive, such as a queue consumer or a file watcher.
Anything that puts itself in the background leaves nothing for grat to watch, so that is the one thing it cannot manage.
Where that boundary sits. grat follows the port back to the process holding it and up its chain of parents. If that chain does not lead to the command grat started, the service never becomes ready, and grat says so rather than pretending.
That rules out anything whose port is held by a separate daemon. A container started through Docker is the common case: the listener belongs to Docker, not to your command.
Most of that config writes itself. grat discover reads the project and proposes what it finds. Angular, Astro, Next.js, Nuxt, React Router, SvelteKit and Vite are recognised on the frontend, Django, Laravel, Symfony, Rails, Phoenix, Spring Boot, FastAPI, Flask and ASP.NET Core on the backend, along with Go modules, Rust crates using Axum or Actix, Deno and Bun projects, Hugo and Jekyll sites, and Swift packages using Vapor. A Docusaurus site or a Storybook is reported in addition to the application it sits beside, on a port of its own. Every command it writes carries the port and the host, because a development server that binds every interface is on the network by accident.
Sometimes it asks you instead, and that is the point. Express, Fastify, NestJS and Go read no port of their own and have no flag for one, so the port sits in the line of your code that starts the server. grat looks there for process.env.PORT or os.Getenv("PORT"). If it finds it, you get a command. If it does not, grat names the file and the reason rather than guessing, because a guessed port produces a service that runs perfectly and never reports ready. You then either read the port from the environment, or name the command once with --service name=command.
One tool it recognises in order to refuse. Eleventy's development server binds every interface and its argument parser rejects --host, so nothing keeps the site off the network, and it moves to another port when the assigned one is taken. grat reports an Eleventy project with that reason rather than writing the one command in your config that would do both.
Some repositories you cannot write into. grat discover --registry keeps the configuration in grat's own registry and leaves the directory untouched. Everything else is the same, including the port it reserves against every other project on the machine, and grat status says where the configuration came from. The file stays the default because it travels with a clone and with a branch, so a colleague types grat start and it works.
The environment is small on purpose. A command inherits ten variables, namely HOME, LANG, LC_ALL, LC_CTYPE, LOGNAME, PATH, SHELL, TERM, TMPDIR and USER, plus whatever it lists in inherit_env. Everything else you happen to have exported stays out, so a service behaves the same however your shell is set up.
A process that has been launched is not a service that answers. grat waits for a listener owned by the command it started, then for the health path to return, and only then calls it ready.
Each command runs in its own process session, with the port it was given in PORT and a small, deliberate environment.
Signals reach the process group grat created, and only after the live PID, start identity and group all match what it recorded.
Output goes straight to .grat/log/, so a service keeps its log after the command that started it has exited.
Ports stop being a thing you remember. A service's role decides the range it may live in, and grat allocates inside it, across every project it knows about.
Whether you want to show somebody on the other side of the world how your site or web app currently looks, or you need a webhook URL that reaches your local development environment, grat gives you a way to do it. Underneath, the exposing runs on Tailscale. Where it is missing, grat asks first: it says what Tailscale is, shows the exact command, and waits for a yes. Say no and nothing on your machine changes, and everything else in grat carries on working. Say yes and grat installs it and says so as it goes. Two steps stay with you, because they cannot be taken on your behalf. The background service starts with administrator rights, so the system asks for your password, and the sign-in runs through your browser because that is where your account is.
Running the command is the decision, so nothing has to be prepared first. Where only a callback has business with the internet, name the path and everything else stays inside.
Exposing service example.com [info] Tailscale is not installed on this machine [info] Command brew install tailscale [...] Tailscale installing [ok] Tailscale installed [info] Tailnet this machine is signed in to no tailnet [...] Sign-in opening the page in your browser [ok] Tailnet this machine is connected [...] backend publishing /api/webhooks/creem [ok] backend reachable at https://mini.tail1234.ts.net/api/webhooks/creem