# Pitchfork

> Comprehensive guide for pitchfork, a daemon/process manager for developers (by jdx, author of mise). Use this skill whenever the user mentions pitchfork, pitchfork.toml, or needs help managing background processes and long-running dev services: starting/stopping/restarting daemons, running a command in the background, viewing/following daemon logs, defining services in pitchfork.toml, the shell hook for auto start/stop on entering/leaving a project directory, ready checks (delay, output regex, HTTP, TCP port, custom command), file-watch restarts, cron-scheduled tasks, dependency chains and groups, lifecycle hooks (on_ready/on_fail/on_retry/on_stop/on_exit), auto-restart/retry on failure, resource limits, the reverse proxy with stable *.localhost URLs, the TUI dashboard, boot/supervisor management, container mode, or its MCP server. Also use when the user wants a lighter alternative to pm2/foreman/overmind/systemd for local dev, or wants to prevent duplicate background processes.

- Skill: `knowsuchagency/pitchfork` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add knowsuchagency/pitchfork`
- Raw SKILL.md: https://api.skillmd.com/api/skills/knowsuchagency/pitchfork/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: knowsuchagency (https://skillmd.com/u/knowsuchagency)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/knowsuchagency/pitchfork

---


# Pitchfork

Pitchfork is a "devilishly good" daemon/process manager for developers, written in Rust by
[jdx](https://github.com/jdx) (author of mise). It starts a background service only if it isn't
already running (no duplicate processes), auto-restarts on failure with backoff, resolves
dependency chains, supports ready checks/file-watching/cron, and can auto start/stop daemons as you
`cd` in and out of a project via a shell hook. Monitor through a terminal TUI or web dashboard, and
drive it from AI assistants through a built-in MCP server.

- Docs: https://pitchfork.jdx.dev
- Repo: https://github.com/jdx/pitchfork
- CLI reference: https://pitchfork.jdx.dev/cli.html
- Config reference: https://pitchfork.jdx.dev/reference/configuration.html

> Tip: run `pitchfork <command> --help` for the exact, version-specific flags on any subcommand.
> This skill documents pitchfork ≥ 2.10.

## When to reach for pitchfork

- You keep starting the same dev server twice and leaking orphan processes — pitchfork refuses to
  start a daemon that's already running.
- You want services to come up automatically when you `cd` into a project and shut down when you
  leave (the shell hook).
- You have a stack (db → cache → api → worker) with startup ordering and readiness gating.
- You want pm2/foreman/overmind/systemd ergonomics for *local* dev without the weight.

## Installation

```sh
mise use -g pitchfork            # Recommended (jdx's own tool)
cargo install pitchfork-cli      # From crates.io
brew install pitchfork           # Homebrew
# or download a prebuilt binary from https://github.com/jdx/pitchfork/releases
pitchfork --version              # Verify
```

### Shell activation (the shell hook — enables auto start/stop)

The shell hook is what makes pitchfork "automatically start daemons when you enter a project
directory and stop them when you leave." It hooks your shell's directory-change so that, on each
`cd`, pitchfork reconciles the daemons declared in the nearest `pitchfork.toml`. Add the activation
line to your shell config, then restart your shell:

```sh
echo 'eval "$(pitchfork activate bash)"' >> ~/.bashrc                       # Bash
echo 'eval "$(pitchfork activate zsh)"' >> ~/.zshrc                         # Zsh
echo 'pitchfork activate fish | source' >> ~/.config/fish/config.fish       # Fish
```

Supported shells: **bash, zsh, fish**. See https://pitchfork.jdx.dev/guides/shell-hook.html.

How the hook behaves (per the shell-hook guide):

- On **entering** a directory, daemons whose config includes `auto = ["start"]` are started.
- On **leaving**, daemons with `auto = ["stop"]` are stopped — but only after a brief delay (so a
  quick `cd out && cd back` doesn't bounce the service), and only once **no terminal sessions
  remain inside the project directory**. Open another terminal still `cd`'d into the project and the
  service keeps running.

### Tab completions (optional)

```sh
pitchfork completion bash > ~/.local/share/bash-completion/completions/pitchfork
pitchfork completion zsh  > ~/.zfunc/_pitchfork
pitchfork completion fish > ~/.config/fish/completions/pitchfork.fish
```

### Start at boot (optional)

`boot` registers the supervisor with the OS init system (launchd on macOS, systemd on Linux), so
daemons marked `boot_start = true` come up at login/startup.

```sh
pitchfork boot enable      # User-level (~/Library/LaunchAgents or ~/.config/systemd/user)
sudo pitchfork boot enable # System-level, for all users (/Library/LaunchDaemons or /etc/systemd/system)
pitchfork boot status
pitchfork boot disable
```

## Core commands

```sh
pitchfork run <ID> -- <CMD>...   # Run an ad-hoc command in the background under name ID (no toml needed)
pitchfork start [ID...]          # Start daemon(s) defined in pitchfork.toml; -l/-g/-a for local/global/all
pitchfork stop [ID...]           # Stop daemon(s); -a/-l/-g; graceful SIGTERM then SIGKILL
pitchfork restart <ID>           # Stop then start a daemon
pitchfork status <ID>            # Detailed status of one daemon (PID, status)
pitchfork list                   # Table of all daemons + state (alias: ls; --hide-header to omit header)
pitchfork logs [ID...]           # Show/tail logs; omit ID for all daemons
pitchfork wait <ID>              # Block until the daemon STOPS, tailing its logs (exits with its code)
pitchfork enable <ID>            # Allow a daemon to start (undo disable)
pitchfork disable <ID>           # Prevent a daemon from starting/restarting
pitchfork clean                  # Drop stopped/failed entries from the list (alias: c)
pitchfork tui                    # Interactive terminal dashboard
```

Most commands have short aliases: `r` run, `s` start, `kill` stop, `ls` list, `l` logs, `w` wait,
`e` enable, `d` disable, `c` clean, `stat` status, `cfg` config, `sup` supervisor.

### `run` — one-off background daemons (no config file)

`run` is the fastest way to background a command. The name (`<ID>`) makes it idempotent and
addressable by every other command.

```sh
pitchfork run web -- npm run dev               # Background a dev server named "web"
pitchfork run api -- ./server --port 8080
pitchfork run api -f -- ./server               # -f/--force: restart if "api" already running
pitchfork run api --retry 3 -- ./server        # Restart up to 3 times on failure
pitchfork run api -d 5 -- ./server             # Consider ready after a 5s delay (default 3s)
pitchfork run api -o 'Listening on' -- ./server  # Ready when stdout matches this regex
pitchfork run api --http http://localhost:8080/health -- ./server  # Ready on HTTP 2xx
pitchfork run api --port 8080 -- ./server      # Ready when TCP port 8080 is listening
pitchfork run api --cmd 'pg_isready' -- ./server  # Ready when this shell command exits 0
pitchfork run web --expected-port 3000 --bump -- npm run dev  # Find a free port if 3000 is taken
pitchfork run build -q -- ./build.sh           # -q/--quiet: suppress startup log output
```

> Everything after `--` is the command and its args, run verbatim — no shell parsing surprises.

### `start` / `stop` — daemons from `pitchfork.toml`

```sh
pitchfork start api              # Start one daemon defined in pitchfork.toml
pitchfork start api worker       # Start several
pitchfork start -l               # All LOCAL daemons in ./pitchfork.toml (alias --all-local)
pitchfork start -g               # All GLOBAL daemons in ~/.config/pitchfork/config.toml (--all-global)
pitchfork start -a               # All daemons, local and global
pitchfork start api -f           # Force restart if already running
pitchfork start api --port 8080  # Override/add a ready check at start time
pitchfork stop -a                # Stop everything in reverse dependency order
```

`start` waits for each daemon's ready check to pass before returning. `stop` does a graceful
shutdown: SIGTERM, wait ~3s (fast polling), then SIGKILL if still alive. With `-a/-l/-g`, daemons
stop in **reverse dependency order** (dependents before their dependencies).

### `logs` — view and follow output

```sh
pitchfork logs api                 # All logs for "api" (paged if long)
pitchfork logs api worker          # Multiple daemons
pitchfork logs                     # All daemons
pitchfork logs api -n 50           # Last 50 lines
pitchfork logs api -f              # Follow in real time (alias --tail/--follow)
pitchfork logs api --since 5min    # Relative window: 5min, 2h, 1d
pitchfork logs api --since '10:30' --until '12:00'   # Time window (today)
pitchfork logs api --since '2024-01-15 10:00:00'     # Absolute datetime
pitchfork logs api --raw -n 100    # Raw lines, no color/formatting (good for piping)
pitchfork logs api --clear         # Delete logs for "api"  (pitchfork logs --clear = all)
```

Logs are stored under the pitchfork state dir (e.g. `~/.local/state/pitchfork/logs`).

### `wait` — block until a daemon stops

`wait` blocks until the daemon **exits**, streaming its logs, and returns the daemon's exit code.
Use it to chain on a one-shot daemon or keep a script attached to a long-running one:

```sh
pitchfork wait migrate && echo "migration done"
```

> Note: `wait` waits for the daemon to **stop**. To block until a service is **ready** and then
> continue, use a ready check (`--port`, `--http`, `--output`, `--cmd`, `--delay`) on `run`/`start`,
> which already blocks until ready before returning.

## Configuration: `pitchfork.toml`

Define daemons in a `pitchfork.toml` at your project root. Each `[daemons.<id>]` block describes one
service. A `config.toml` in `~/.config/pitchfork/` (or `/etc/pitchfork/`) holds **global** daemons
available everywhere.

```toml
# Optional: prefix this file's daemon names, e.g. referenced as "frontend/web"
namespace = "frontend"

[daemons.postgres]
run = "postgres -D ./data"
auto = ["start", "stop"]        # auto-start on cd-in, auto-stop on cd-out (needs the shell hook)
ready_port = 5432               # ready when this TCP port accepts connections
dir = "db"                      # working directory (relative to the toml, or absolute)

[daemons.redis]
run = "redis-server"
auto = ["start", "stop"]
ready_output = "Ready to accept connections"   # ready when stdout matches this regex

[daemons.api]
run = "npm run dev:api"
depends = ["postgres", "redis"] # started after deps are ready (topological; independents in parallel)
ready_http = "http://localhost:8080/health"    # ready when this URL returns 2xx
retry = 3                       # auto-restart up to 3 times on failure (true = unlimited)
watch = ["src/**/*.ts"]         # restart when matching files change
env = { NODE_ENV = "development", PORT = "8080" }

[daemons.worker]
run = "npm run worker"
depends = ["redis"]
ready_delay = 2                 # just wait N seconds, then consider ready

[daemons.cleanup]
run = "./scripts/cleanup.sh"
cron = "0 0 2 * * *"            # run on a 6-field cron schedule (sec min hour dom mon dow): 2am daily
```

### Daemon keys

| Key | Type | Purpose |
|-----|------|---------|
| `run` | string (required) | The command to execute. |
| `dir` | string | Working directory (relative to the toml, or absolute). |
| `env` | table | Environment variables, e.g. `{ NODE_ENV = "development" }`. |
| `user` | string | Run as this user/uid (e.g. `"postgres"` or `"501"`); needs privileges. |
| `auto` | array | Shell-hook lifecycle: `"start"` (on cd-in), `"stop"` (on cd-out). |
| `depends` | array | Daemons that must be ready first; topological, independents run in parallel. Cross-namespace via `"global/postgres"`. |
| `retry` | int or bool | Auto-restart on failure: a count, or `true` for unlimited (with backoff). |
| `boot_start` | bool | Start this daemon when the supervisor starts at boot. |
| `mise` | bool | Run the command through mise (load `mise.toml` tools/env first). |
| **Ready checks** | | (pick one; all block `start`/`run` until satisfied) |
| `ready_delay` | int | Seconds to wait, then mark ready. |
| `ready_output` | string | Regex; ready when daemon output matches. |
| `ready_http` | string or table | URL that must return 2xx, or `{ url = "...", status = [200, 401] }`. |
| `ready_port` | int | TCP port that must be listening. |
| `ready_cmd` | string | Shell command polled until it exits 0. |
| **File watching** | | |
| `watch` | array | Glob patterns; matching changes trigger a restart. |
| `watch_mode` | string | `"native"`, `"poll"`, or `"auto"` (default). |
| **Ports / proxy** | | |
| `port` | int / array / table | Expected port(s); table form `{ expect = [3000], bump = 10 }` auto-finds a free port. |
| **Scheduling** | | |
| `cron` | string or table | 6-field cron (`sec min hour dom mon dow`), or `{ schedule, retrigger, immediate }`. |
| **Resource limits** | | |
| `memory_limit` | string | e.g. `"512MB"`, `"2GiB"` — restart/kill if exceeded. |
| `cpu_limit` | number | CPU percent cap (100 = one core; `200` = two). |
| `stop_signal` | string or table | Signal for graceful stop, e.g. `"SIGINT"` or `{ signal = "SIGINT", timeout = "5s" }`. |

> Deprecated port keys you may see in older configs: `expected_port`, `auto_bump_port`,
> `port_bump_attempts`. Prefer the unified `port` key.

### Lifecycle hooks — `[daemons.<id>.hooks]`

Run commands at lifecycle transitions (see https://pitchfork.jdx.dev/guides/lifecycle-hooks.html):

```toml
[daemons.api]
run = "npm run dev:api"
ready_http = "http://localhost:8080/health"

[daemons.api.hooks]
on_ready = "curl -X POST https://alerts.example.com/ready"  # daemon passed its ready check
on_fail  = "./scripts/cleanup.sh"                           # daemon failed
on_retry = "echo 'retrying...'"                             # before each retry attempt
on_stop  = "./cleanup.sh"                                   # explicitly stopped by pitchfork
on_exit  = "./teardown.sh"                                  # any termination (clean exit, crash, or stop)
# on_output supports filtering: { run = "...", regex = "ERROR", debounce = "2s" }
```

### Cron scheduling

`cron` uses a **6-field** expression: `second minute hour day-of-month month day-of-week`
(Sunday = 0). The inline-table form adds a retrigger policy:

```toml
[daemons.report]
run = "./generate-report.sh"
cron = { schedule = "0 0 2 * * *", retrigger = "finish", immediate = false }
```

`retrigger` controls what happens when the schedule fires while the previous run is still active:

| Value | Behavior |
|-------|----------|
| `finish` (default) | Only retrigger once the previous run has finished — no overlap. |
| `always` | Kill the running instance and start fresh — always run the latest. |
| `success` | Only retrigger if the previous run exited 0. |
| `fail` | Only retrigger if the previous run failed (auto-retry logic). |

See https://pitchfork.jdx.dev/guides/scheduling.html.

### Groups

Group daemons to start/stop several at once:

```toml
[groups.backend]
daemons = ["postgres", "redis", "api", "worker"]
```

```sh
pitchfork start backend     # start the whole group
```

### Managing config from the CLI

`config add` writes a `[daemons.<id>]` block into the nearest `pitchfork.toml`:

```sh
pitchfork config                 # List all pitchfork.toml files from the cwd upward
pitchfork config add api -- npm run dev          # Command after --
pitchfork config add api --run 'npm start' --retry 3
pitchfork config add api --run 'npm start' --watch 'src/**/*.ts'
pitchfork config add api --run 'npm start' --autostart --autostop   # sets auto = ["start","stop"]
pitchfork config add worker --run './worker' --depends api
pitchfork config add report --run './report.sh' \
  --cron-schedule '0 0 2 * * *' --cron-retrigger finish
pitchfork config add api --run './server' \
  --ready-http http://localhost:8080/health      # also: --ready-output, --port, --ready-cmd, --delay
pitchfork config add api --run './server' --on-ready 'curl .../ready' --on-fail './cleanup.sh'
pitchfork config remove api      # Remove a daemon (alias: rm)
```

Namespaced ids are supported, e.g. `pitchfork config add frontend/api ...`.

## Auto start/stop (the day-to-day workflow)

With the shell hook installed (`pitchfork activate`), mark daemons `auto = ["start", "stop"]`.
Entering the project directory starts them; leaving stops them (after a short delay, once no
terminals remain in the dir). Services come up exactly when you're working and clean up when you
move on — no manual `start`/`stop` needed.

```toml
[daemons.web]
run = "npm run dev"
auto = ["start", "stop"]
ready_port = 3000
```

Use `auto = ["start"]` alone to start on entry but leave it running, or `auto = ["stop"]` to only
clean up on exit.

## Reverse proxy — stable `*.localhost` URLs

The proxy maps a stable slug URL (e.g. `https://myapp.localhost`) to a daemon's actual port, so
bookmarks and configs don't break when a port changes or gets bumped. Slugs live in the global
config under `[slugs]`.

```sh
pitchfork proxy trust         # Install the proxy's self-signed TLS cert into the system trust store
pitchfork proxy add ...       # Map a slug → project dir + daemon
pitchfork proxy status        # Show all slugs and their state
pitchfork proxy remove ...    # (alias: rm)
```

Enable it in config:

```toml
[settings.proxy]
enable = true
```

## TUI dashboard

```sh
pitchfork tui     # Interactive terminal dashboard (start/stop/restart, view logs, vim-style keys)
```

`pitchfork list` gives a quick text overview; `pitchfork logs -f` follows output. A browser-based
web dashboard is also available — see https://pitchfork.jdx.dev/guides/tui.html.

## Supervisor

A background supervisor process tracks daemons and performs restarts/scheduling/file-watching. The
shell hook and `boot enable` manage it for you, but you can drive it directly:

```sh
pitchfork supervisor start    # Start the supervisor in the background
pitchfork supervisor status
pitchfork supervisor stop
pitchfork supervisor run      # Run it in the foreground (debugging)
```

## Container mode

For Docker/CI, pitchfork can run as PID 1 / foreground supervisor that brings up the whole stack and
keeps the container alive. See https://pitchfork.jdx.dev/guides/container-mode.html.

## MCP server (AI assistants)

Pitchfork ships an MCP server so assistants like Claude Code and Cursor can manage daemons directly:

```sh
pitchfork mcp        # Speaks MCP over stdin/stdout; wire into your assistant's MCP config
```

Example Claude Code registration:

```sh
claude mcp add pitchfork -- pitchfork mcp
```

Exposed tools: `pitchfork_status` (list daemons + PID/status/errors), `pitchfork_start`
(with `force` to restart), `pitchfork_stop`, `pitchfork_restart`, and `pitchfork_logs`
(`n` lines, optional daemon `id`s). See https://pitchfork.jdx.dev/guides/mcp.html.

## Quick recipes

Background a dev server and follow it:

```sh
pitchfork run web -- npm run dev
pitchfork logs web -f
```

A full stack with ordering and readiness, auto-managed on `cd`:

```toml
[daemons.db]
run = "docker compose up postgres"
auto = ["start", "stop"]
ready_port = 5432

[daemons.api]
run = "npm run dev:api"
depends = ["db"]
auto = ["start", "stop"]
ready_http = "http://localhost:8080/health"
retry = 3
watch = ["src/**/*.ts"]

[daemons.web]
run = "npm run dev:web"
depends = ["api"]
auto = ["start", "stop"]
ready_port = 3000
```

Then just `cd` into the project (with the shell hook active) and everything comes up in order;
`cd` away and it tears down.

## Tips & gotchas

- Pitchfork won't start a daemon that's already running — re-running `start`/`run` is safe and
  idempotent (it's how you avoid duplicate dev servers).
- Prefer `depends` + a `ready_*` check over manual ordering or `sleep` — pitchfork blocks on
  readiness and parallelizes independent daemons.
- `ready_*` blocks until **ready**; `wait` blocks until **stopped**. Don't confuse them.
- The shell hook only stops a daemon once **no** terminal sessions remain in the project dir — a
  second terminal still in the directory keeps services alive.
- Cron is **6 fields** (leading seconds), unlike classic 5-field crontab.
- `pitchfork clean` clears stale stopped/failed rows from `list`; it never touches running daemons
  or your config.
- Global daemons live in `~/.config/pitchfork/config.toml`; reference them across projects as
  `global/<name>` (or your configured namespace) in `depends`/`groups`.
- Run `pitchfork <command> --help` for exact, version-specific flags.

