air-go

Installation
SKILL.md

Air — Go Hot Reload

Air watches Go source files and rebuilds/restarts your app on changes. Essential for web server development where you want sub-second feedback.

Install

go install github.com/air-verse/air@latest

Basic usage

Run air in your project root. With no config, it watches .go files, rebuilds, and restarts. For anything non-trivial, use a .air.toml.

.air.toml

root = "."
tmp_dir = "tmp"

[build]
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ."
  delay = 500
  exclude_dir = ["tmp", "k8s", "cmd", "node_modules", "vendor"]
  exclude_regex = ["_test\\.go$"]
  include_ext = ["go", "html", "css", "js", "yml", "yaml", "toml"]
  kill_delay = "1s"
  send_interrupt = true
  stop_on_error = true

[misc]
  clean_on_exit = true

Key settings explained

Setting What it does Why it matters
delay Milliseconds to wait after a file change before rebuilding Prevents rapid-fire rebuilds when saving multiple files
kill_delay Time to wait after killing the old process before starting the new one Critical for SQLite/file locks — the old process needs time to release resources
send_interrupt Send SIGINT before SIGKILL Allows graceful shutdown (flush writes, close DB connections)
stop_on_error Don't restart on build errors Prevents crash loops when you have a syntax error
include_ext File extensions to watch Add html, css, js to reload on frontend changes too
exclude_dir Directories to ignore Exclude test fixtures, k8s manifests, vendored deps
exclude_regex File patterns to ignore Skip test files to avoid rebuilding when only tests change
clean_on_exit Remove tmp dir on exit Prevents stale binaries from confusing the next run

Makefile integration

.PHONY: dev dev-email dev-clean

# Dev: console output, fresh state
dev: dev-clean
	air

# Dev: real emails, LAN-accessible for mobile testing
dev-email: dev-clean
	$(call setup_env, .env.prod)
	SMTP_HOST=smtp.example.com \
	BASE_URL=http://$(LAN_IP):8080 \
	air

dev-clean:
	@rm -f app.db app.db-shm app.db-wal tmp/main
	@-lsof -ti :8080 | xargs kill -9 2>/dev/null || true

The dev-clean target is important — it kills anything on the port and removes stale DB files before starting fresh.

Passing environment variables

Air inherits the parent shell's environment. Set env vars inline before air:

# Inline
PORT=3000 DEBUG=true air

# From .env file in Makefile
$(call setup_env, .env.dev)
air

Do NOT put env vars in .air.toml — it doesn't support that. Environment is always from the shell.

Common issues and fixes

"address already in use"

The previous process didn't die. Fix:

lsof -ti :8080 | xargs kill -9

Or add to dev-clean target as shown above.

"database is locked" (SQLite)

Air kills the old process and starts the new one, but SQLite WAL files can linger. Fix with these .air.toml settings:

[build]
  kill_delay = "1s"          # Give old process time to release the lock
  send_interrupt = true      # Graceful shutdown via SIGINT
  pre_cmd = ["rm -f app.db-shm app.db-wal"]  # Clean stale lock files

If using an in-memory DB or ephemeral dev DB, just delete it on each rebuild:

[build]
  pre_cmd = ["rm -f app.db app.db-shm app.db-wal"]

"too many open files" on large projects

Air's file watcher can hit OS limits. Exclude unnecessary directories:

[build]
  exclude_dir = ["tmp", "vendor", "node_modules", ".git", "k8s", "docs"]

Air exits immediately in background/CI

Air is a foreground tool — it watches stdin for Ctrl+C. In scripts or CI, use go run . instead. Air is for interactive development only.

Changes not detected

Check that your file extension is in include_ext and the directory isn't in exclude_dir. Air only watches extensions you explicitly list.

Watching non-Go files

To rebuild on HTML/CSS/JS changes (useful for apps that serve static files):

[build]
  include_ext = ["go", "html", "css", "js"]

Air will rebuild the Go binary even when only frontend files change. This is fine — Go builds are fast and the restart picks up the new static files.

Multi-binary projects

If your project has multiple binaries (cmd/server, cmd/worker), point cmd at the one you want:

[build]
  cmd = "go build -o ./tmp/server ./cmd/server"
  bin = "./tmp/server"

Run a second air instance in another terminal for the worker if needed, with a separate config:

air -c .air.worker.toml
Related skills
Installs
6
First Seen
Apr 18, 2026