Repository: air-verse/air

Issue:
Support forcing color
`fatih/color` disables colors if the output isn't a tty. The accepted way of dealing with this seems to be for the program using `color` to expose a flag and hard-override
https://github.com/fatih/color/issues/179

Some sort of flag for this would be really appreciated, but not really a deal-breaker tbh. Thanks!

Discussion:
@Matthewaj: For anyone who wants a temporary work around and is using Mac OS or Linux, [unbuffer](https://linux.die.net/man/1/unbuffer) from the expect package makes it appear like air is running from a tty. 

Just simply run `unbuffer air`

https://formulae.brew.sh/formula/expect

Solution patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 209fa62..97da4ce 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,7 +1,7 @@
 # These are supported funding model platforms
 
-github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
 patreon: cosmtrek
+github: xiantang
 open_collective: # Replace with a single Open Collective username
 ko_fi: # Replace with a single Ko-fi username
 tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..33a5b8d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,29 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: ''
+assignees: xiantang
+
+---
+
+**Describe the bug**  
+Provide a clear and concise description of the issue you encountered.
+
+**To Reproduce**  
+- [ ] Provide a minimal reproducible example (ideally as a PR to https://github.com/air-verse/air-reproducible-example).  
+- [ ] Include relevant parts of your `.air.toml` (redact secrets) **or** the exact `air` command you ran.  
+- [ ] Describe the steps to reproduce the issue clearly in that example.
+
+**Expected behavior**  
+Describe clearly and concisely what you expected to happen.
+
+**Screenshots**  
+If applicable, include screenshots to help illustrate the issue.
+
+**Desktop (please complete the following information):**  
+- OS: [e.g., macOS 13.3.1]  
+- Air Version: [e.g., v1.63.2]
+
+**Additional context**  
+- Please ensure your issue is written in English as the primary language.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..bbcbbe7
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6f70b88..3b9517e 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -19,11 +19,13 @@ jobs:
         id: go
         uses: actions/setup-go@v5
         with:
-          go-version: ^1.23
-      - name: Install golangci-lint
-        uses: golangci/golangci-lint-action@v6
+          go-version: ^1.25
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v8
         with:
-          version: v1.61
+          # Build golangci-lint from source with the configured Go version
+          install-mode: goinstall
+          version: latest
       - name: Install dependency
         run: if [ $(uname) == "Darwin" ]; then brew install gnu-sed ;fi
       - name: Build
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 0fb8d33..35a7c7c 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,7 +16,7 @@ jobs:
       - name: Setup Go
         uses: actions/setup-go@v5
         with:
-          go-version: ^1.23
+          go-version: ^1.25
 
       - name: Set GOVERSION
         run: echo "GOVERSION=$(go version | sed -r 's/go version go(.*)\ .*/\1/')" >> $GITHUB_ENV
diff --git a/.github/workflows/smoke_test.yml b/.github/workflows/smoke_test.yml
index 123c971..b3b6616 100644
--- a/.github/workflows/smoke_test.yml
+++ b/.github/workflows/smoke_test.yml
@@ -6,14 +6,10 @@ on:
 
 jobs:
   smoke_test_ubuntu:
-    uses: air-verse/air/.github/workflows/smoke_test_reuse_job.yml@master
+    uses: ./.github/workflows/smoke_test_reuse_job.yml
     with:
       run_on: ubuntu-latest
   smoke_test_macos:
-    uses: air-verse/air/.github/workflows/smoke_test_reuse_job.yml@master
+    uses: ./.github/workflows/smoke_test_reuse_job.yml
     with:
       run_on: macos-latest
-  smoke_test_windows:
-    uses: air-verse/air/.github/workflows/smoke_test_reuse_job_windows.yml@master
-    with:
-      run_on: windows-latest
diff --git a/.github/workflows/smoke_test_reuse_job.yml b/.github/workflows/smoke_test_reuse_job.yml
index 7760904..3b1a2d5 100644
--- a/.github/workflows/smoke_test_reuse_job.yml
+++ b/.github/workflows/smoke_test_reuse_job.yml
@@ -13,12 +13,17 @@ jobs:
     runs-on: ${{ inputs.run_on }}
     steps:
       - name: Check out code
-        uses: actions/checkout@v4
+        uses: actions/checkout@v5
       - name: Setup Go
         id: go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@v6
         with:
-          go-version: ^1.23
+          go-version: ^1.25
+      - name: golangci-lint
+        uses: golangci/golangci-lint-action@v8
+        with:
+          install-mode: goinstall
+          version: latest
       - name: Install
         run: make install
       - name: Check rebuild
diff --git a/.github/workflows/smoke_test_reuse_job_windows.yml b/.github/workflows/smoke_test_reuse_job_windows.yml
deleted file mode 100644
index 4a01aa6..0000000
--- a/.github/workflows/smoke_test_reuse_job_windows.yml
+++ /dev/null
@@ -1,39 +0,0 @@
-name: Reusable smoke test on Windows
-
-on:
-  workflow_call:
-    inputs:
-      run_on:
-        required: true
-        type: string
-
-jobs:
-  smoke_test:
-    name: Smoke test
-    runs-on: ${{ inputs.run_on }}
-    steps:
-      - name: Check out code
-        uses: actions/checkout@v4
-      - name: Setup Go
-        id: go
-        uses: actions/setup-go@v4
-        with:
-          go-version: ^1.23
-      - name: Setup Python
-        uses: actions/setup-python@v5
-        with:
-          python-version: "3.x"
-      - name: Install depend
-        run: |
-          python -m pip install pexpect
-      - name: Install
-        run: make install
-      - name: Check rebuild
-        id: check_rebuild
-        working-directory: ./smoke_test
-        run: |
-          python smoke_test.py
-      - uses: nick-invision/assert-action@v2
-        with:
-          expected: "PASS"
-          actual: ${{ steps.check_rebuild.outputs.value }}
diff --git a/.golangci.yml b/.golangci.yml
index ddadae4..6dc64d0 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,18 +1,33 @@
-run:
-  timeout: 2m
-
+version: "2"
 linters:
-  disable-all: true
+  default: none
   enable:
-    - copyloopvar # detects places where loop variables are copied
-    - errcheck # Errcheck is a program for checking for unchecked errors in go programs.
-    - gci # Gci controls Go package import order and makes it always deterministic
-    - goimports # checks that goimports was run
-    - ineffassign # Detects when assignments to existing variables are not used
-    - misspell # spell checker
-    - revive # configurable linter for Go. Drop-in replacement of golint
-    - staticcheck # go vet on steroids
-    - stylecheck # static analysis, finds bugs and performance issues, offers simplifications, and enforces style rules
-    - testifylint # checks usage of github.com/stretchr/testify
-    - unconvert # Remove unnecessary type conversions
-    - unused # Checks Go code for unused constants, variables, functions and types
+    - copyloopvar
+    - errcheck
+    - ineffassign
+    - misspell
+    - revive
+    - staticcheck
+    - testifylint
+    - unconvert
+    - unused
+  exclusions:
+    generated: lax
+    presets:
+      - comments
+      - common-false-positives
+      - legacy
+      - std-error-handling
+    paths:
+      - third_party$
+      - builtin$
+      - examples$
+formatters:
+  enable:
+    - goimports
+  exclusions:
+    generated: lax
+    paths:
+      - third_party$
+      - builtin$
+      - examples$
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..c0bad2b
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,86 @@
+# AGENTS
+
+Guidelines for contributors and AI coding agents working in this repository.
+
+## Goals
+
+- Keep changes minimal, focused, and idiomatic to Go.
+- Prefer root‑cause fixes over band‑aids; don’t refactor unrelated code.
+- Maintain user‑facing behavior and CLI flags unless explicitly changing them.
+
+## Project Snapshot
+
+- Language: Go (modules enabled)
+- Entry: `main.go`
+- Core package: `runner/` (watcher, engine, flags, config, proxy)
+- Docs: `README*.md`, `air_example.toml`, `docs/`
+- Tooling: `Makefile`, `hack/check.sh`, `hooks/pre-commit`
+
+## Quick Start
+
+- Build: `make build`
+- Install: `make install`
+- CI setup: `make ci`
+- Lint/format/check staged files: `make check`
+- Run tests: `go test ./...`
+
+Tip: run `make init` once to install `goimports`, set up `golangci-lint`, and enable the pre‑commit hook.
+
+## Code Style (Go)
+
+- Formatting: rely on `goimports` (invoked by `hack/check.sh`).
+- Linting: fixes must satisfy `golangci-lint` (same config used in CI).
+- Errors: wrap with context; return early; avoid panics in library code.
+- Concurrency: use contexts where appropriate; avoid data races; prefer channels or mutexes over ad‑hoc globals.
+- Logging: keep logs concise, actionable, and consistent with existing patterns.
+- Public API/flags: changing behavior or flags requires README updates and tests.
+
+## Tests
+
+- Location: alongside code as `*_test.go` (see `runner/*_test.go`).
+- Scope: unit tests near behavior changes; table‑driven where it fits.
+- Run locally: `go test ./...`
+
+## Common Change Points
+
+- Config fields: edit `runner/config.go`, update parsing, defaults, and tests; reflect in `README.md` and `air_example.toml`.
+- CLI flags: update `runner/flag.go`, sync help text, and README usage.
+- Watcher behavior: `runner/watcher.go`; add tests for edge cases (create/delete/move events, include/exclude rules).
+- Proxy/browser reload: `runner/proxy*.go`; keep docs aligned with README’s proxy section.
+
+## Tooling and Commands
+
+- `make init`: installs tooling and pre‑commit hook.
+- `make check`: runs formatting + `golangci-lint` via `hack/check.sh`.
+- `make build|install|ci`: standard workflows.
+- Prefer `rg` for repository searches; keep file reads to small chunks.
+
+## Documentation
+
+- Update `README.md` for any user‑visible change (flags, config, examples).
+- Keep examples in `air_example.toml` accurate when adding/removing fields.
+- Include concise migration notes in PRs when behavior changes.
+
+## PR & Commit Guidelines
+
+- Commit messages: imperative, present tense, concise summary line.
+- Include rationale and tradeoffs in the PR description.
+- Link related issues; note breaking changes clearly.
+
+## For AI Coding Agents (Codex CLI)
+
+- Planning: for multi‑step tasks, maintain an explicit plan and mark progress.
+- Edits: use a single focused patch per logical change; prefer `apply_patch`.
+- Preambles: briefly state what you’re about to do before running commands.
+- Searches: prefer `rg` and read files in ≤250‑line chunks.
+- Validation: run `make check` and `go test ./...` to verify changes.
+- Scope control: avoid touching unrelated files; don’t reformat the repo wholesale.
+- No external network: assume offline; do not add dependencies lightly.
+
+## Release Notes (maintainers)
+
+- Follow the README “Release” section for tagging; CI performs builds.
+
+---
+
+Questions or uncertain scope? Open an issue or ask for clarification before implementing.
diff --git a/Dockerfile b/Dockerfile
index 24c8760..e35c021 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.23 AS builder
+FROM golang:1.25 AS builder
 
 LABEL maintainer="Rick Yu <cosmtrek@gmail.com>"
 
@@ -12,7 +12,7 @@ RUN --mount=type=cache,target=/go/pkg/mod go mod download
 
 RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build make ci && make install
 
-FROM golang:1.23
+FROM golang:1.25
 
 COPY --from=builder /go/bin/air  /go/bin/air
 
diff --git a/Makefile b/Makefile
index 0bd28b9..61c3fe3 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,8 @@ LDFLAGS += -X "main.airVersion=$(AIRVER)"
 LDFLAGS += -X "main.goVersion=$(shell go version | sed -r 's/go version go(.*)\ .*/\1/')"
 
 GO := GO111MODULE=on CGO_ENABLED=0 go
-GOLANGCI_LINT_VERSION = v1.61.0
+GOLANGCI_LINT_VERSION = v2.6.1
+GOLANGCI_LINT_CURRENT := $(shell golangci-lint --version 2>/dev/null | sed -n 's/.*version \([0-9.]*\).*/v\1/p')
 
 .PHONY: init
 init: install-golangci-lint
@@ -15,9 +16,8 @@ init: install-golangci-lint
 
 .PHONY: install-golangci-lint
 install-golangci-lint:
-ifeq (, $(shell which golangci-lintx))
-	@$(shell curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOPATH)/bin $(GOLANGCI_LINT_VERSION))
-endif
+	@echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)"
+	@GO111MODULE=on go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
 
 .PHONY: setup
 setup: init
@@ -29,7 +29,7 @@ check:
 
 .PHONY: ci
 ci: init
-	@$(GO) mod tidy && $(GO) mod vendor
+	@$(GO) mod tidy
 
 .PHONY: build
 build: check
diff --git a/README-ja.md b/README-ja.md
index 2be664e..cb1d14f 100644
--- a/README-ja.md
+++ b/README-ja.md
@@ -43,25 +43,38 @@ air --help
 もしビルドコマンドと起動コマンドを設定したい場合は、設定ファイルを使わずに以下のようにコマンドを使うことができます:
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api"
 ```
 
 入力値としてリストを取る引数には、アイテムを区切るためにコンマを使用します:
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build.exclude_dir "templates,build"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api" --build.exclude_dir "templates,build"
 ```
 
+従来の `build.bin` フィールドは非推奨で、今後のリリースで削除される予定です。代わりに `build.entrypoint` を使ってください。
+
 ## インストール
 
 ### `go install` を使う場合（推奨）
 
-go 1.23以上を使う場合:
+go 1.25以上を使う場合:
 
-```bash
+```shell
 go install github.com/air-verse/air@latest
 ```
 
+### `go get -tool` を使う場合
+
+go 1.24以上を使う場合:
+
+```shell
+go get -tool github.com/air-verse/air@latest
+
+# 使い方は以下の通りです:
+go tool air -v
+```
+
 ### `install.sh` を使う場合
 
 ```shell
@@ -84,6 +97,12 @@ curl -sSfL https://goblin.run/github.com/air-verse/air | sh
 curl -sSfL https://goblin.run/github.com/air-verse/air | PREFIX=/tmp sh
 ```
 
+### ソフトウェアパッケージマネージャー [mise](https://github.com/jdx/mise) を使う場合
+
+```shell
+mise use -g air
+```
+
 ### Docker/Podman
 
 [cosmtrek/air](https://hub.docker.com/r/cosmtrek/air) という Docker イメージをプルしてください。
@@ -219,7 +238,7 @@ services:
 
 ```Dockerfile
 # 1.16以上の利用したいバージョンを選択してください
-FROM golang:1.23-alpine
+FROM golang:1.25-alpine
 
 WORKDIR /app
 
diff --git a/README-zh_cn.md b/README-zh_cn.md
index 2a5d87d..f9b68d6 100644
--- a/README-zh_cn.md
+++ b/README-zh_cn.md
@@ -29,25 +29,38 @@ Air 是为 Go 应用开发设计的另外一个热重载的命令行工具。只
 如果你只是想配置构建命令和运行命令，您可以直接使用以下命令，而无需配置文件:
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api"
 ```
 
 对于以列表形式输入的参数，使用逗号来分隔项目:
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build.exclude_dir "templates,build"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api" --build.exclude_dir "templates,build"
 ```
 
+旧的 `build.bin` 字段已弃用，将在未来移除，请改用 `build.entrypoint`。
+
 ## 安装
 
 ### 使用 `go install` （推荐）
 
-使用 go 1.23 或更高版本:
+使用 go 1.25 或更高版本:
 
 ```shell
 go install github.com/air-verse/air@latest
 ```
 
+### 使用 `go get -tool`
+
+使用 go 1.24 或更高版本:
+
+```shell
+go get -tool github.com/air-verse/air@latest
+
+# 然后像这样使用：
+go tool air -v
+```
+
 ### 使用 install.sh
 
 ```shell
@@ -70,6 +83,12 @@ curl -sSfL https://goblin.run/github.com/cosmtrek/air | sh
 curl -sSfL https://goblin.run/github.com/cosmtrek/air | PREFIX=/tmp sh
 ```
 
+### 使用软件包管理器 [mise](https://github.com/jdx/mise)
+
+```shell
+mise use -g air
+```
+
 ### Docker/Podman
 
 请拉取这个 Docker 镜像 [cosmtrek/air](https://hub.docker.com/r/cosmtrek/air).
@@ -205,7 +224,7 @@ services:
 
 ```Dockerfile
 # 选择你想要的版本，>= 1.16
-FROM golang:1.23-alpine
+FROM golang:1.25-alpine
 
 WORKDIR /app
 
diff --git a/README-zh_tw.md b/README-zh_tw.md
index 8643b07..1f60af2 100644
--- a/README-zh_tw.md
+++ b/README-zh_tw.md
@@ -29,25 +29,38 @@ Air 是一個另類的自動重新編譯執行命令列工具，用於開發 Go
 如果你想設定建置命令和執行命令，你可以在不需要配置檔案的情況下如下使用命令：
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api"`
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api"
 ```
 
 對於需要輸入列表的參數，可以使用逗號將項目分隔：
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build.exclude_dir "templates,build"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api" --build.exclude_dir "templates,build"
 ```
 
+舊的 `build.bin` 欄位已被棄用，未來版本會移除，請改用 `build.entrypoint`。
+
 ## 安裝
 
 ### 使用 `go install` （推薦）
 
-需要使用 go 1.23 或更高版本：
+需要使用 go 1.25 或更高版本：
 
-```bash
+```shell
 go install github.com/air-verse/air@latest
 ```
 
+### 使用 `go get -tool`
+
+需要使用 go 1.24 或更高版本：
+
+```shell
+go get -tool github.com/air-verse/air@latest
+
+# 然後這樣使用：
+go tool air -v
+```
+
 ### 透過 install.sh
 
 ```shell
@@ -70,9 +83,15 @@ curl -sSfL https://goblin.run/github.com/air-verse/air | sh
 curl -sSfL https://goblin.run/github.com/air-verse/air | PREFIX=/tmp sh
 ```
 
+### 使用軟體套件管理器 [mise](https://github.com/jdx/mise)
+
+```shell
+mise use -g air
+```
+
 ### 透過 `go install`
 
-使用 go 1.18 或更高版本:
+使用 go 1.25 或更高版本:
 
 ```bash
 go install github.com/air-verse/air@latest
@@ -212,7 +231,7 @@ services:
 
 ```Dockerfile
 # Choose whatever you want, version >= 1.16
-FROM golang:1.21-alpine
+FROM golang:1.25-alpine
 
 WORKDIR /app
 
diff --git a/README.md b/README.md
index f027fda..85c8aa8 100644
--- a/README.md
+++ b/README.md
@@ -43,25 +43,36 @@ air --help
 If you want to config build command and run command, you can use like the following command without the config file:
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api"
 ```
 
 Use a comma to separate items for arguments that take a list as input:
 
 ```shell
-air --build.cmd "go build -o bin/api cmd/run.go" --build.bin "./bin/api" --build.exclude_dir "templates,build"
+air --build.cmd "go build -o bin/api cmd/run.go" --build.entrypoint "./bin/api" --build.exclude_dir "templates,build"
 ```
 
 ## Installation
 
 ### Via `go install` (Recommended)
 
-With go 1.23 or higher:
+With go 1.25 or higher:
 
-```bash
+```shell
 go install github.com/air-verse/air@latest
 ```
 
+### Via `go get -tool` (project install)
+
+With go 1.24 or higher:
+
+```shell
+go get -tool github.com/air-verse/air@latest
+
+# then use it like so:
+go tool air -v
+```
+
 ### Via install.sh
 
 ```shell
@@ -84,6 +95,12 @@ curl -sSfL https://goblin.run/github.com/air-verse/air | sh
 curl -sSfL https://goblin.run/github.com/air-verse/air | PREFIX=/tmp sh
 ```
 
+### Using software package manager [mise](https://github.com/jdx/mise)
+
+```shell
+mise use -g air
+```
+
 ### Docker/Podman
 
 Please pull this Docker image [cosmtrek/air](https://hub.docker.com/r/cosmtrek/air).
@@ -191,6 +208,25 @@ air -- -h
 air -c .air.toml -- -h
 ```
 
+### Entrypoint
+
+Use `build.entrypoint` to point at the binary generated by `build.cmd` and describe how it should be executed. The value is an array where the first element is the executable (resolved relative to `root` unless it lacks a path separator, in which case `$PATH` is consulted) and every subsequent element is treated as a default argument. Values from `build.args_bin` and the command line are appended after the inline arguments. The legacy `build.bin` field is deprecated and will be removed in a future release, so prefer the entrypoint form going forward.
+
+```toml
+[build]
+entrypoint = ["./tmp/main"]
+args_bin = ["server", ":8080"]
+
+# Inline the default arguments directly after the binary.
+entrypoint = ["./tmp/main", "server", ":8080"]
+
+# Use PATH-resolved tools like dlv by omitting path separators.
+entrypoint = [
+  "dlv", "exec", "--accept-multiclient", "--log", "--headless", "--continue",
+  "--listen=:8999", "--api-version", "2", "./tmp/main",
+]
+```
+
 ### Docker Compose
 
 ```yaml
@@ -219,7 +255,7 @@ services:
 
 ```Dockerfile
 # Choose whatever you want, version >= 1.16
-FROM golang:1.23-alpine
+FROM golang:1.25-alpine
 
 WORKDIR /app
 
diff --git a/air_example.toml b/air_example.toml
index 32d45cc..80e4229 100644
--- a/air_example.toml
+++ b/air_example.toml
@@ -1,3 +1,5 @@
+#:schema https://json.schemastore.org/any.json
+
 # Config file for [Air](https://github.com/air-verse/air) in TOML format
 
 # Working directory
@@ -12,8 +14,10 @@ pre_cmd = ["echo 'hello air' > pre_cmd.txt"]
 cmd = "go build -o ./tmp/main ."
 # Array of commands to run after ^C
 post_cmd = ["echo 'hello air' > post_cmd.txt"]
-# Binary file yields from `cmd`.
+# Binary file yields from 'cmd', will be deprecated soon, recommend using entrypoint.
 bin = "tmp/main"
+# Entrypoint binary relative to root. First item is the executable, more items are default arguments.
+entrypoint = ["./tmp/main"]
 # Customize binary, can setup environment variables when run your app.
 full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
 # Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
diff --git a/go.mod b/go.mod
index eea9d62..00e88b1 100644
--- a/go.mod
+++ b/go.mod
@@ -1,46 +1,34 @@
 module github.com/air-verse/air
 
-go 1.23
+go 1.25
 
 require (
-	dario.cat/mergo v1.0.1
-	github.com/creack/pty v1.1.23
-	github.com/fatih/color v1.17.0
-	github.com/fsnotify/fsnotify v1.7.0
-	github.com/gohugoio/hugo v0.134.3
+	dario.cat/mergo v1.0.2
+	github.com/fatih/color v1.18.0
+	github.com/fsnotify/fsnotify v1.9.0
+	github.com/gohugoio/hugo v0.149.1
 	github.com/pelletier/go-toml v1.9.5
-	github.com/stretchr/testify v1.9.0
+	github.com/stretchr/testify v1.11.1
 )
 
 require (
 	github.com/bep/debounce v1.2.1 // indirect
-	github.com/bep/godartsass v1.2.0 // indirect
-	github.com/bep/godartsass/v2 v2.1.0 // indirect
+	github.com/bep/godartsass/v2 v2.5.0 // indirect
 	github.com/bep/golibsass v1.2.0 // indirect
 	github.com/bep/gowebp v0.4.0 // indirect
-	github.com/bep/lazycache v0.5.0 // indirect
-	github.com/cli/safeexec v1.0.1 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/dlclark/regexp2 v1.11.4 // indirect
-	github.com/getkin/kin-openapi v0.127.0 // indirect
-	github.com/gobuffalo/flect v1.0.3 // indirect
 	github.com/gobwas/glob v0.2.3 // indirect
-	github.com/mattn/go-colorable v0.1.13 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
-	github.com/mattn/go-runewidth v0.0.16 // indirect
-	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.4 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rivo/uniseg v0.4.7 // indirect
-	github.com/spf13/afero v1.11.0 // indirect
-	github.com/spf13/cast v1.7.0 // indirect
-	github.com/tdewolff/parse/v2 v2.7.15 // indirect
-	github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 // indirect
-	golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect
-	golang.org/x/image v0.20.0 // indirect
-	golang.org/x/sys v0.25.0 // indirect
-	golang.org/x/text v0.18.0 // indirect
-	golang.org/x/tools v0.25.0 // indirect
-	google.golang.org/protobuf v1.34.2 // indirect
+	github.com/spf13/afero v1.14.0 // indirect
+	github.com/spf13/cast v1.9.2 // indirect
+	github.com/tdewolff/parse/v2 v2.8.3 // indirect
+	golang.org/x/sys v0.35.0 // indirect
+	golang.org/x/text v0.28.0 // indirect
+	google.golang.org/protobuf v1.36.8 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 1920544..e3ec7ca 100644
--- a/go.sum
+++ b/go.sum
@@ -1,72 +1,62 @@
-cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
-dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
-dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
 github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69 h1:+tu3HOoMXB7RXEINRVIpxJCT+KdYiI7LAEAUrOw3dIU=
 github.com/BurntSushi/locker v0.0.0-20171006230638-a6e239ea1c69/go.mod h1:L1AbZdiDllfyYH5l5OkAaZtk7VkWe89bPJFmnDBNHxg=
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
-github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
+github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NTz+9sGw=
+github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
 github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c h1:651/eoCRnQ7YtSjAnSzRucrJz+3iGEFt+ysraELS81M=
 github.com/armon/go-radix v1.0.1-0.20221118154546-54df44f2176c/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
+github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
+github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
 github.com/bep/clocks v0.5.0 h1:hhvKVGLPQWRVsBP/UB7ErrHYIO42gINVbvqxvYTPVps=
 github.com/bep/clocks v0.5.0/go.mod h1:SUq3q+OOq41y2lRQqH5fsOoxN8GbxSiT6jvoVVLCVhU=
 github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
 github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
-github.com/bep/gitmap v1.6.0 h1:sDuQMm9HoTL0LtlrfxjbjgAg2wHQd4nkMup2FInYzhA=
-github.com/bep/gitmap v1.6.0/go.mod h1:n+3W1f/rot2hynsqEGxGMErPRgT41n9CkGuzPvz9cIw=
+github.com/bep/gitmap v1.9.0 h1:2pyb1ex+cdwF6c4tsrhEgEKfyNfxE34d5K+s2sa9byc=
+github.com/bep/gitmap v1.9.0/go.mod h1:Juq6e1qqCRvc1W7nzgadPGI9IGV13ZncEebg5atj4Vo=
 github.com/bep/goat v0.5.0 h1:S8jLXHCVy/EHIoCY+btKkmcxcXFd34a0Q63/0D4TKeA=
 github.com/bep/goat v0.5.0/go.mod h1:Md9x7gRxiWKs85yHlVTvHQw9rg86Bm+Y4SuYE8CTH7c=
-github.com/bep/godartsass v1.2.0 h1:E2VvQrxAHAFwbjyOIExAMmogTItSKodoKuijNrGm5yU=
-github.com/bep/godartsass v1.2.0/go.mod h1:6LvK9RftsXMxGfsA0LDV12AGc4Jylnu6NgHL+Q5/pE8=
-github.com/bep/godartsass/v2 v2.1.0 h1:fq5Y1xYf4diu4tXABiekZUCA+5l/dmNjGKCeQwdy+s0=
-github.com/bep/godartsass/v2 v2.1.0/go.mod h1:AcP8QgC+OwOXEq6im0WgDRYK7scDsmZCEW62o1prQLo=
+github.com/bep/godartsass/v2 v2.5.0 h1:tKRvwVdyjCIr48qgtLa4gHEdtRkPF8H1OeEhJAEv7xg=
+github.com/bep/godartsass/v2 v2.5.0/go.mod h1:rjsi1YSXAl/UbsGL85RLDEjRKdIKUlMQHr6ChUNYOFU=
 github.com/bep/golibsass v1.2.0 h1:nyZUkKP/0psr8nT6GR2cnmt99xS93Ji82ZD9AgOK6VI=
 github.com/bep/golibsass v1.2.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
+github.com/bep/goportabletext v0.1.0 h1:8dqym2So1cEqVZiBa4ZnMM1R9l/DnC1h4ONg4J5kujw=
+github.com/bep/goportabletext v0.1.0/go.mod h1:6lzSTsSue75bbcyvVc0zqd1CdApuT+xkZQ6Re5DzZFg=
 github.com/bep/gowebp v0.4.0 h1:QihuVnvIKbRoeBNQkN0JPMM8ClLmD6V2jMftTFwSK3Q=
 github.com/bep/gowebp v0.4.0/go.mod h1:95gtYkAA8iIn1t3HkAPurRCVGV/6NhgaHJ1urz0iIwc=
-github.com/bep/imagemeta v0.8.1 h1:tjZLPRftjxU7PTI87o5e5WKOFQ4S9S0engiP1OTpJTI=
-github.com/bep/imagemeta v0.8.1/go.mod h1:5piPAq5Qomh07m/dPPCLN3mDJyFusvUG7VwdRD/vX0s=
-github.com/bep/lazycache v0.5.0 h1:9FJRrEp/s3BUpGEfTvLhmv50N4dXzoZnyRPU6NOUv0w=
-github.com/bep/lazycache v0.5.0/go.mod h1:NmRm7Dexh3pmR1EignYR8PjO2cWybFQ68+QgY3VMCSc=
+github.com/bep/helpers v0.6.0 h1:qtqMCK8XPFNM9hp5Ztu9piPjxNNkk8PIyUVjg6v8Bsw=
+github.com/bep/helpers v0.6.0/go.mod h1:IOZlgx5PM/R/2wgyCatfsgg5qQ6rNZJNDpWGXqDR044=
+github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
+github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
+github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
+github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk=
 github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
 github.com/bep/logg v0.4.0/go.mod h1:Ccp9yP3wbR1mm++Kpxet91hAZBEQgmWgFgnXX3GkIV0=
-github.com/bep/overlayfs v0.9.2 h1:qJEmFInsW12L7WW7dOTUhnMfyk/fN9OCDEO5Gr8HSDs=
-github.com/bep/overlayfs v0.9.2/go.mod h1:aYY9W7aXQsGcA7V9x/pzeR8LjEgIxbtisZm8Q7zPz40=
+github.com/bep/overlayfs v0.10.0 h1:wS3eQ6bRsLX+4AAmwGjvoFSAQoeheamxofFiJ2SthSE=
+github.com/bep/overlayfs v0.10.0/go.mod h1:ouu4nu6fFJaL0sPzNICzxYsBeWwrjiTdFZdK4lI3tro=
 github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
 github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
-github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
 github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
 github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
-github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
-github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00=
-github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q=
-github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
-github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
-github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0=
-github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/disintegration/gift v1.2.1 h1:Y005a1X4Z7Uc+0gLpSAsKhWi4qLtsdEcMIbbdvdZ6pc=
 github.com/disintegration/gift v1.2.1/go.mod h1:Jh2i7f7Q2BM7Ezno3PhfezbR1xpUg9dUg3/RlKGr4HI=
-github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo=
-github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
-github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
-github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evanw/esbuild v0.23.1 h1:ociewhY6arjTarKLdrXfDTgy25oxhTZmzP8pfuBTfTA=
-github.com/evanw/esbuild v0.23.1/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
-github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
-github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
+github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
+github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
+github.com/evanw/esbuild v0.25.9 h1:aU7GVC4lxJGC1AyaPwySWjSIaNLAdVEEuq3chD0Khxs=
+github.com/evanw/esbuild v0.25.9/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48=
+github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
+github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
 github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
-github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
-github.com/getkin/kin-openapi v0.127.0 h1:Mghqi3Dhryf3F8vR370nN67pAERW+3a95vomb3MAREY=
-github.com/getkin/kin-openapi v0.127.0/go.mod h1:OZrfXzUfGrNbsKj+xmFBx6E5c6yH3At/tAKSc2UszXM=
+github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
+github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
+github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
+github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -79,51 +69,35 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e h1:QArsSubW7eDh8APMXkByjQWvuljwPGAGQpJEFn0F0wY=
 github.com/gohugoio/go-i18n/v2 v2.1.3-0.20230805085216-e63c13218d0e/go.mod h1:3Ltoo9Banwq0gOtcOwxuHG6omk+AwsQPADyw2vQYOJQ=
-github.com/gohugoio/hashstructure v0.1.0 h1:kBSTMLMyTXbrJVAxaKI+wv30MMJJxn9Q8kfQtJaZ400=
-github.com/gohugoio/hashstructure v0.1.0/go.mod h1:8ohPTAfQLTs2WdzB6k9etmQYclDUeNsIHGPAFejbsEA=
+github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg=
+github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
 github.com/gohugoio/httpcache v0.7.0 h1:ukPnn04Rgvx48JIinZvZetBfHaWE7I01JR2Q2RrQ3Vs=
 github.com/gohugoio/httpcache v0.7.0/go.mod h1:fMlPrdY/vVJhAriLZnrF5QpN3BNAcoBClgAyQd+lGFI=
-github.com/gohugoio/hugo v0.134.3 h1:Pn2KECXAAQWCd2uryDcmtzVhNJWGF5Pt6CplQvLcWe0=
-github.com/gohugoio/hugo v0.134.3/go.mod h1:/1gnGxlWfAzQarxcQ+tMvKw4e/IMBwy0DFbRxORwOtY=
-github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0 h1:MNdY6hYCTQEekY0oAfsxWZU1CDt6iH+tMLgyMJQh/sg=
-github.com/gohugoio/hugo-goldmark-extensions/extras v0.2.0/go.mod h1:oBdBVuiZ0fv9xd8xflUgt53QxW5jOCb1S+xntcN4SKo=
-github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0 h1:7PY5PIJ2mck7v6R52yCFvvYHvsPMEbulgRviw3I9lP4=
-github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.0/go.mod h1:r8g5S7bHfdj0+9ShBog864ufCsVODKQZNjYYY8OnJpM=
+github.com/gohugoio/hugo v0.149.1 h1:uWOc8Ve4h4e48FyYhBquRoHCJviyxA5yGrFJLT48yio=
+github.com/gohugoio/hugo v0.149.1/go.mod h1:HS6BP6e8FGxungP4CHC3zeLDvhBLnTJIjHJZWTZjs7o=
+github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0 h1:dco+7YiOryRoPOMXwwaf+kktZSCtlFtreNdiJbETvYE=
+github.com/gohugoio/hugo-goldmark-extensions/extras v0.5.0/go.mod h1:CRrxQTKeM3imw+UoS4EHKyrqB7Zp6sAJiqHit+aMGTE=
+github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1 h1:nUzXfRTszLliZuN0JTKeunXTRaiFX6ksaWP0puLLYAY=
+github.com/gohugoio/hugo-goldmark-extensions/passthrough v0.3.1/go.mod h1:Wy8ThAA8p2/w1DY05vEzq6EIeI2mzDjvHsu7ULBVwog=
 github.com/gohugoio/locales v0.14.0 h1:Q0gpsZwfv7ATHMbcTNepFd59H7GoykzWJIxi113XGDc=
 github.com/gohugoio/locales v0.14.0/go.mod h1:ip8cCAv/cnmVLzzXtiTpPwgJ4xhKZranqNqtoIu0b/4=
 github.com/gohugoio/localescompressed v1.0.1 h1:KTYMi8fCWYLswFyJAeOtuk/EkXR/KPTHHNN9OS+RTxo=
 github.com/gohugoio/localescompressed v1.0.1/go.mod h1:jBF6q8D7a0vaEmcWPNcAjUZLJaIVNiwvM3WlmTvooB0=
-github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
-github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
-github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
-github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
-github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
-github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
-github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
-github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
-github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
-github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/hairyhenderson/go-codeowners v0.5.0 h1:dpQB+hVHiRc2VVvc2BHxkuM+tmu9Qej/as3apqUbsWc=
-github.com/hairyhenderson/go-codeowners v0.5.0/go.mod h1:R3uW1OQXEj2Gu6/OvZ7bt6hr0qdkLvUWPiqNaWnexpo=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
+github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
+github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTAF2xLgo4a4RwAo=
+github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
-github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
-github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
 github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
 github.com/jdkato/prose v1.2.1/go.mod h1:AiRHgVagnEx2JbQRQowVBKjG0bcs/vtkGCH1dYAL1rA=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
-github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
 github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@@ -138,133 +112,93 @@ github.com/makeworld-the-better-one/dither/v2 v2.4.0 h1:Az/dYXiTcwcRSe59Hzw4RI1r
 github.com/makeworld-the-better-one/dither/v2 v2.4.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc=
 github.com/marekm4/color-extractor v1.2.1 h1:3Zb2tQsn6bITZ8MBVhc33Qn1k5/SEuZ18mrXGUqIwn0=
 github.com/marekm4/color-extractor v1.2.1/go.mod h1:90VjmiHI6M8ez9eYUaXLdcKnS+BAOp7w+NpwBdkJmpA=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
+github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE=
 github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/muesli/smartcrop v0.3.0 h1:JTlSkmxWg/oQ1TcLDoypuirdE8Y/jzNirQeLkxpA6Oc=
 github.com/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
-github.com/niklasfasching/go-org v1.7.0 h1:vyMdcMWWTe/XmANk19F4k8XGBYg0GQ/gJGMimOjGMek=
-github.com/niklasfasching/go-org v1.7.0/go.mod h1:WuVm4d45oePiE0eX25GqTDQIt/qPW1T9DGkRscqLW5o=
-github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
-github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
+github.com/niklasfasching/go-org v1.9.1 h1:/3s4uTPOF06pImGa2Yvlp24yKXZoTYM+nsIlMzfpg/0=
+github.com/niklasfasching/go-org v1.9.1/go.mod h1:ZAGFFkWvUQcpazmi/8nHqwvARpr1xpb+Es67oUGX/48=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
+github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
+github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
+github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
+github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
+github.com/olekukonko/tablewriter v1.0.9 h1:XGwRsYLC2bY7bNd93Dk51bcPZksWZmLYuaTHR0FqfL8=
+github.com/olekukonko/tablewriter v1.0.9/go.mod h1:5c+EBPeSqvXnLLgkm9isDdzR3wjfBkHR9Nhfp3NWrzo=
 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
 github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
 github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
-github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
-github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
 github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
 github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
 github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
-github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
-github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
-github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
-github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
-github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
-github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-github.com/tdewolff/minify/v2 v2.20.37 h1:Q97cx4STXCh1dlWDlNHZniE8BJ2EBL0+2b0n92BJQhw=
-github.com/tdewolff/minify/v2 v2.20.37/go.mod h1:L1VYef/jwKw6Wwyk5A+T0mBjjn3mMPgmjjA688RNsxU=
-github.com/tdewolff/parse/v2 v2.7.15 h1:hysDXtdGZIRF5UZXwpfn3ZWRbm+ru4l53/ajBRGpCTw=
-github.com/tdewolff/parse/v2 v2.7.15/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
-github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
-github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
-github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
-github.com/tetratelabs/wazero v1.8.0 h1:iEKu0d4c2Pd+QSRieYbnQC9yiFlMS9D+Jr0LsRmcF4g=
-github.com/tetratelabs/wazero v1.8.0/go.mod h1:yAI0XTsMBhREkM/YDAK/zNou3GoiAce1P6+rp/wQhjs=
-github.com/yuin/goldmark v1.7.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
-github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-github.com/yuin/goldmark-emoji v1.0.3 h1:aLRkLHOuBR2czCY4R8olwMjID+tENfhyFDMCRhbIQY4=
-github.com/yuin/goldmark-emoji v1.0.3/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk=
-golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY=
-golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
-golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
-golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
-golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
-golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0=
-golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
-golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
-golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
-golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
-golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
-golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
+github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
+github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
+github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
+github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
+github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tdewolff/minify/v2 v2.24.2 h1:vnY3nTulEAbCAAlxTxPPDkzG24rsq31SOzp63yT+7mo=
+github.com/tdewolff/minify/v2 v2.24.2/go.mod h1:1JrCtoZXaDbqioQZfk3Jdmr0GPJKiU7c1Apmb+7tCeE=
+github.com/tdewolff/parse/v2 v2.8.3 h1:5VbvtJ83cfb289A1HzRA9sf02iT8YyUwN84ezjkdY1I=
+github.com/tdewolff/parse/v2 v2.8.3/go.mod h1:Hwlni2tiVNKyzR1o6nUs4FOF07URA+JLBLd6dlIXYqo=
+github.com/tdewolff/test v1.0.11 h1:FdLbwQVHxqG16SlkGveC0JVyrJN62COWTRyUFzfbtBE=
+github.com/tdewolff/test v1.0.11/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
+github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
+github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
+github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0=
+github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds=
+github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
+github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
+github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
+golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b h1:DXr+pvt3nC887026GRP39Ej11UATqWDmWuS99x26cD0=
+golang.org/x/exp v0.0.0-20250819193227-8b4c13bb791b/go.mod h1:4QTo5u+SEIbbKW1RacMZq1YEfOBqeXa19JeshGi+zc4=
+golang.org/x/image v0.30.0 h1:jD5RhkmVAnjqaCUXfbGBrn3lpxbknfN9w2UhHHU+5B4=
+golang.org/x/image v0.30.0/go.mod h1:SAEUTxCCMWSrJcCy/4HwavEsfZZJlYxeHLc6tTiAe/c=
+golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
+golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
+golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
+golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
+golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
+golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
-golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
-golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
-golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
-golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
-golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE=
-golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
-google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
-google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
-google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
-google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
-google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
-google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
-google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
-google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
-google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
-google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
-google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
-google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
-google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
-google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
+golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
+golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
+golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
+google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
+google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
 gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
+rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
diff --git a/hack/check.sh b/hack/check.sh
index 89563ed..6b33c10 100755
--- a/hack/check.sh
+++ b/hack/check.sh
@@ -19,12 +19,28 @@ if [[ "${#files[@]}" -ne 0 ]]; then
 fi
 
 echo -e "${green}2. Linting"
-out=$(golangci-lint run)
-if [[ -n "${out}" ]]; then
-    echo "${red}${out}"
+if ! command -v golangci-lint &> /dev/null; then
+    echo "${red}golangci-lint command not found. Please install it first."
     exit_code=1
-fi
+else
+    # If golangci-lint was built with an older Go than the module target, hint to upgrade
+    lint_go_ver=$(golangci-lint --version 2>/dev/null | sed -n 's/.*built with go\([0-9]\+\.[0-9]\+\).*/\1/p')
+    mod_go_ver=$(sed -n 's/^go \([0-9]\+\.[0-9]\+\).*/\1/p' go.mod | head -n1)
+    if [[ -n "${lint_go_ver}" && -n "${mod_go_ver}" ]]; then
+        lint_minor=$(echo "${lint_go_ver}" | cut -d. -f2)
+        mod_minor=$(echo "${mod_go_ver}" | cut -d. -f2)
+        if [[ ${lint_minor} -lt ${mod_minor} ]]; then
+            echo "${red}can't load config: the Go language version (go${lint_go_ver}) used to build golangci-lint is lower than the targeted Go version (${mod_go_ver})"
+            echo "${red}Hint: upgrade golangci-lint (run: make init)"
+            exit_code=1
+        fi
+    fi
 
+    if [[ ${exit_code} -eq 0 ]] && ! golangci-lint run; then
+        echo "${red}Linting issues found."
+        exit_code=1
+    fi
+fi
 
 if [[ ${exit_code} -ne 0 ]]; then
     echo "${red}Please fix the errors above :)"
diff --git a/main.go b/main.go
index 4e558bd..3ce5ea2 100644
--- a/main.go
+++ b/main.go
@@ -11,12 +11,14 @@ import (
 	"syscall"
 
 	"github.com/air-verse/air/runner"
+	"github.com/fatih/color"
 )
 
 var (
 	cfgPath     string
 	debugMode   bool
 	showVersion bool
+	colorMode   string
 	cmdArgs     map[string]runner.TomlInfo
 )
 
@@ -39,6 +41,7 @@ func parseFlag(args []string) {
 	flag.StringVar(&cfgPath, "c", "", "config path")
 	flag.BoolVar(&debugMode, "d", false, "debug mode")
 	flag.BoolVar(&showVersion, "v", false, "show version")
+	flag.StringVar(&colorMode, "color", "auto", "colored output: auto, always, never")
 	cmd := flag.CommandLine
 	cmdArgs = runner.ParseConfigFlag(cmd)
 	if err := flag.CommandLine.Parse(args); err != nil {
@@ -81,6 +84,16 @@ func printSplash() {
 }
 
 func main() {
+	switch colorMode {
+	case "always":
+		color.NoColor = false
+	case "never":
+		color.NoColor = true
+	case "auto", "":
+		// do nothing
+	default:
+		log.Fatal("unsupported color mode: use always, never, auto")
+	}
 	if showVersion {
 		printSplash()
 		return
diff --git a/runner/config.go b/runner/config.go
index ebfceeb..92c4e5f 100644
--- a/runner/config.go
+++ b/runner/config.go
@@ -5,10 +5,12 @@ import (
 	"flag"
 	"fmt"
 	"os"
+	"os/exec"
 	"path/filepath"
 	"reflect"
 	"regexp"
 	"runtime"
+	"strings"
 	"time"
 
 	"dario.cat/mergo"
@@ -17,8 +19,9 @@ import (
 
 const (
 	dftTOML = ".air.toml"
-	dftConf = ".air.conf"
 	airWd   = "air_wd"
+
+	schemaHeader = "#:schema https://json.schemastore.org/any.json"
 )
 
 // Config is the main configuration structure for Air.
@@ -34,13 +37,54 @@ type Config struct {
 	Proxy       cfgProxy  `toml:"proxy"`
 }
 
+type entrypoint []string
+
+func (e *entrypoint) UnmarshalTOML(v interface{}) error {
+	switch val := v.(type) {
+	case nil:
+		*e = nil
+		return nil
+	case string:
+		*e = []string{val}
+		return nil
+	case []interface{}:
+		values := make([]string, len(val))
+		for i, raw := range val {
+			s, ok := raw.(string)
+			if !ok {
+				return fmt.Errorf("entrypoint values must be strings, got %T", raw)
+			}
+			values[i] = s
+		}
+		*e = values
+		return nil
+	default:
+		return fmt.Errorf("entrypoint must be a string or array of strings, got %T", v)
+	}
+}
+
+func (e entrypoint) binary() string {
+	if len(e) == 0 {
+		return ""
+	}
+	return e[0]
+}
+
+func (e entrypoint) args() []string {
+	if len(e) <= 1 {
+		return nil
+	}
+	return e[1:]
+}
+
 type cfgBuild struct {
 	PreCmd           []string      `toml:"pre_cmd" usage:"Array of commands to run before each build"`
 	Cmd              string        `toml:"cmd" usage:"Just plain old shell command. You could use 'make' as well"`
+	Entrypoint       entrypoint    `toml:"entrypoint" usage:"Binary file plus optional arguments relative to root, prefer [\"./tmp/main\", \"arg\"] form"`
 	PostCmd          []string      `toml:"post_cmd" usage:"Array of commands to run after ^C"`
-	Bin              string        `toml:"bin" usage:"Binary file yields from 'cmd'"`
+	Bin              string        `toml:"bin,omitempty" usage:"Binary file yields from 'cmd', will be deprecated soon, recommend using entrypoint."`
 	FullBin          string        `toml:"full_bin" usage:"Customize binary, can setup environment variables when run your app"`
-	ArgsBin          []string      `toml:"args_bin" usage:"Add additional arguments when running binary (bin/full_bin)."`
+	ArgsBin          []string      `toml:"args_bin,omitempty" usage:"Add additional arguments when running binary (bin/full_bin)."`
 	Log              string        `toml:"log" usage:"This log file is placed in your tmp_dir"`
 	IncludeExt       []string      `toml:"include_ext" usage:"Watch these filename extensions"`
 	ExcludeDir       []string      `toml:"exclude_dir" usage:"Ignore these filename extensions or directories"`
@@ -59,12 +103,39 @@ type cfgBuild struct {
 	Rerun            bool          `toml:"rerun" usage:"Rerun binary or not"`
 	RerunDelay       int           `toml:"rerun_delay" usage:"Delay after each execution"`
 	regexCompiled    []*regexp.Regexp
+	includeDirAbs    []string
+	extraIncludeDirs []string
 }
 
 func (c *cfgBuild) RegexCompiled() ([]*regexp.Regexp, error) {
 	return c.regexCompiled, nil
 }
 
+func (c *cfgBuild) normalizeIncludeDirs(root string) {
+	c.includeDirAbs = c.includeDirAbs[:0]
+	c.extraIncludeDirs = c.extraIncludeDirs[:0]
+	if root == "" {
+		return
+	}
+	for _, dir := range c.IncludeDir {
+		dir = cleanPath(dir)
+		if dir == "" {
+			continue
+		}
+		dir = filepath.Clean(dir)
+		abs := dir
+		if !filepath.IsAbs(dir) {
+			abs = filepath.Join(root, dir)
+		}
+		abs = filepath.Clean(abs)
+		if isSubPath(root, abs) {
+			c.includeDirAbs = append(c.includeDirAbs, abs)
+			continue
+		}
+		c.extraIncludeDirs = append(c.extraIncludeDirs, abs)
+	}
+}
+
 type cfgLog struct {
 	AddTime  bool `toml:"time" usage:"Show log time"`
 	MainOnly bool `toml:"main_only" usage:"Only show main log (silences watcher, build, runner)"`
@@ -121,6 +192,7 @@ func InitConfig(path string, cmdArgs map[string]TomlInfo) (cfg *Config, err erro
 			return nil, err
 		}
 	}
+	warnDeprecatedBin(cfg)
 	config := defaultConfig()
 	// get addr
 	ret := &config
@@ -140,16 +212,12 @@ func InitConfig(path string, cmdArgs map[string]TomlInfo) (cfg *Config, err erro
 }
 
 func writeDefaultConfig() (string, error) {
-	confFiles := []string{dftTOML, dftConf}
-
-	for _, fname := range confFiles {
-		fstat, err := os.Stat(fname)
-		if err != nil && !os.IsNotExist(err) {
-			return "", fmt.Errorf("failed to check for existing configuration: %w", err)
-		}
-		if err == nil && fstat != nil {
-			return "", errors.New("configuration already exists")
-		}
+	fstat, err := os.Stat(dftTOML)
+	if err != nil && !os.IsNotExist(err) {
+		return "", fmt.Errorf("failed to check for existing configuration: %w", err)
+	}
+	if err == nil && fstat != nil {
+		return "", errors.New("configuration already exists")
 	}
 
 	file, err := os.Create(dftTOML)
@@ -159,12 +227,18 @@ func writeDefaultConfig() (string, error) {
 	defer file.Close()
 
 	config := defaultConfig()
+	if len(config.Build.Entrypoint) == 0 && config.Build.Bin != "" {
+		config.Build.Entrypoint = entrypoint{config.Build.Bin}
+	}
 	configFile, err := toml.Marshal(config)
 	if err != nil {
 		return "", fmt.Errorf("failed to marshal the default configuration: %w", err)
 	}
 
-	_, err = file.Write(configFile)
+	headers := []byte(schemaHeader + "\n\n")
+	content := append(headers, configFile...)
+
+	_, err = file.Write(content)
 	if err != nil {
 		return "", fmt.Errorf("failed to write to %s: %w", dftTOML, err)
 	}
@@ -173,15 +247,10 @@ func writeDefaultConfig() (string, error) {
 }
 
 func defaultPathConfig() (*Config, error) {
-	// when path is blank, first find `.air.toml`, `.air.conf` in `air_wd` and current working directory, if not found, use defaults
-	for _, name := range []string{dftTOML, dftConf} {
-		cfg, err := readConfByName(name)
-		if err == nil {
-			if name == dftConf && !cfg.Log.Silent {
-				fmt.Println("`.air.conf` will be deprecated soon, recommend using `.air.toml`.")
-			}
-			return cfg, nil
-		}
+	// when path is blank, first find `.air.toml` in `air_wd` and current working directory, if not found, use defaults
+	cfg, err := readConfByName(dftTOML)
+	if err == nil {
+		return cfg, nil
 	}
 
 	dftCfg := defaultConfig()
@@ -206,7 +275,8 @@ func readConfByName(name string) (*Config, error) {
 func defaultConfig() Config {
 	build := cfgBuild{
 		Cmd:          "go build -o ./tmp/main .",
-		Bin:          "./tmp/main",
+		Bin:          "",
+		Entrypoint:   entrypoint{"./tmp/main"},
 		Log:          "build-errors.log",
 		IncludeExt:   []string{"go", "tpl", "tmpl", "html"},
 		IncludeDir:   []string{},
@@ -215,7 +285,7 @@ func defaultConfig() Config {
 		ExcludeFile:  []string{},
 		IncludeFile:  []string{},
 		ExcludeDir:   []string{"assets", "tmp", "vendor", "testdata"},
-		ArgsBin:      []string{},
+		ArgsBin:      nil,
 		ExcludeRegex: []string{"_test.go"},
 		Delay:        1000,
 		Rerun:        false,
@@ -306,7 +376,25 @@ func (c *Config) preprocess(args map[string]TomlInfo) error {
 		ed[i] = cleanPath(ed[i])
 	}
 
+	if len(c.Build.Entrypoint) > 0 {
+		entry := c.Build.Entrypoint.binary()
+		if !filepath.IsAbs(entry) {
+			if resolved := resolveCommandPath(entry); resolved != "" {
+				entry = resolved
+			} else {
+				entry = joinPath(c.Root, entry)
+			}
+		}
+
+		entry, err = filepath.Abs(entry)
+		if err != nil {
+			return err
+		}
+		c.Build.Entrypoint[0] = entry
+	}
+
 	adaptToVariousPlatforms(c)
+	c.Build.normalizeIncludeDirs(c.Root)
 
 	// Join runtime arguments with the configuration arguments
 	runtimeArgs := flag.Args()
@@ -369,9 +457,19 @@ func (c *Config) killDelay() time.Duration {
 }
 
 func (c *Config) binPath() string {
+	if len(c.Build.Entrypoint) > 0 {
+		return c.Build.Entrypoint.binary()
+	}
 	return joinPath(c.Root, c.Build.Bin)
 }
 
+func (c *Config) runnerBin() string {
+	if len(c.Build.Entrypoint) > 0 && len(c.Build.FullBin) == 0 {
+		return c.Build.Entrypoint.binary()
+	}
+	return c.Build.Bin
+}
+
 func (c *Config) tmpPath() string {
 	return joinPath(c.Root, c.TmpDir)
 }
@@ -388,6 +486,18 @@ func (c *Config) rel(path string) string {
 	return s
 }
 
+func resolveCommandPath(entry string) string {
+	if entry == "" || strings.ContainsAny(entry, `/\`) {
+		return ""
+	}
+
+	path, err := exec.LookPath(entry)
+	if err != nil {
+		return ""
+	}
+	return path
+}
+
 // withArgs returns a new config with the given arguments added to the configuration.
 func (c *Config) withArgs(args map[string]TomlInfo) {
 	for _, value := range args {
@@ -400,3 +510,13 @@ func (c *Config) withArgs(args map[string]TomlInfo) {
 	}
 
 }
+
+func warnDeprecatedBin(cfg *Config) {
+	if cfg == nil {
+		return
+	}
+	if cfg.Build.Bin == "" || len(cfg.Build.Entrypoint) > 0 {
+		return
+	}
+	fmt.Fprintln(os.Stdout, "[warning] build.bin is deprecated; set build.entrypoint instead")
+}
diff --git a/runner/engine.go b/runner/engine.go
index 143b936..9762eeb 100644
--- a/runner/engine.go
+++ b/runner/engine.go
@@ -50,6 +50,15 @@ func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) {
 	if err != nil {
 		return nil, err
 	}
+	var entryArgs []string
+	if len(cfg.Build.FullBin) == 0 {
+		entryArgs = cfg.Build.Entrypoint.args()
+	}
+	runArgs := make([]string, 0, len(entryArgs)+len(cfg.Build.ArgsBin))
+	if len(entryArgs) > 0 {
+		runArgs = append(runArgs, entryArgs...)
+	}
+	runArgs = append(runArgs, cfg.Build.ArgsBin...)
 	e := Engine{
 		config:         cfg,
 		exiter:         defaultExiter{},
@@ -57,7 +66,7 @@ func NewEngineWithConfig(cfg *Config, debugMode bool) (*Engine, error) {
 		logger:         logger,
 		watcher:        watcher,
 		debugMode:      debugMode,
-		runArgs:        cfg.Build.ArgsBin,
+		runArgs:        runArgs,
 		eventCh:        make(chan string, 1000),
 		watcherStopCh:  make(chan bool, 10),
 		buildRunCh:     make(chan bool, 1),
@@ -97,7 +106,7 @@ func (e *Engine) Run() {
 	if err = e.checkRunEnv(); err != nil {
 		os.Exit(1)
 	}
-	if err = e.watching(e.config.Root); err != nil {
+	if err = e.watchConfiguredDirs(); err != nil {
 		os.Exit(1)
 	}
 
@@ -117,6 +126,40 @@ func (e *Engine) checkRunEnv() error {
 	return nil
 }
 
+func (e *Engine) watchConfiguredDirs() error {
+	type watchTarget struct {
+		path     string
+		optional bool
+	}
+	targets := []watchTarget{{path: e.config.Root, optional: false}}
+	for _, dir := range e.config.Build.extraIncludeDirs {
+		targets = append(targets, watchTarget{path: dir, optional: true})
+	}
+
+	seen := make(map[string]struct{}, len(targets))
+	for _, target := range targets {
+		if target.path == "" {
+			continue
+		}
+		cleaned := filepath.Clean(target.path)
+		if _, ok := seen[cleaned]; ok {
+			continue
+		}
+		if _, err := os.Stat(cleaned); err != nil {
+			if os.IsNotExist(err) && target.optional {
+				e.watcherLog("include_dir %s does not exist, skipping", e.config.rel(cleaned))
+				continue
+			}
+			return err
+		}
+		if err := e.watching(cleaned); err != nil {
+			return err
+		}
+		seen[cleaned] = struct{}{}
+	}
+	return nil
+}
+
 func (e *Engine) watching(root string) error {
 	return filepath.Walk(root, func(path string, info os.FileInfo, _ error) error {
 		// NOTE: path is absolute
@@ -446,14 +489,12 @@ func (e *Engine) runCommand(command string) error {
 		stdout.Close()
 		stderr.Close()
 	}()
-	_, _ = io.Copy(os.Stdout, stdout)
-	_, _ = io.Copy(os.Stderr, stderr)
+
+	copyOutput(os.Stdout, stdout)
+	copyOutput(os.Stderr, stderr)
+
 	// wait for command to finish
-	err = cmd.Wait()
-	if err != nil {
-		return err
-	}
-	return nil
+	return cmd.Wait()
 }
 
 func (e *Engine) runCommandCopyOutput(command string) (string, error) {
@@ -560,12 +601,16 @@ func (e *Engine) runBin() error {
 			}
 
 			if e.config.Build.StopOnError {
-				cmdBinPath := cmdPath(e.config.rel(e.config.binPath()))
+				relBinPath := e.config.rel(e.config.binPath())
+				if relBinPath == "" || strings.HasPrefix(relBinPath, "..") {
+					return
+				}
+				cmdBinPath := cmdPath(relBinPath)
 				if _, err = os.Stat(cmdBinPath); os.IsNotExist(err) {
 					return
 				}
 				if err = os.Remove(cmdBinPath); err != nil {
-					e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err)
+					e.mainLog("failed to remove %s, error: %s", relBinPath, err)
 				}
 			}
 		}()
@@ -591,7 +636,7 @@ func (e *Engine) runBin() error {
 			case <-killCh:
 				return
 			default:
-				formattedBin := formatPath(e.config.Build.Bin)
+				formattedBin := formatPath(e.config.runnerBin())
 				command := strings.Join(append([]string{formattedBin}, e.runArgs...), " ")
 				cmd, stdout, stderr, err := e.startCmd(command)
 				if err != nil {
@@ -611,15 +656,9 @@ func (e *Engine) runBin() error {
 					e.binStopCh = killFunc(cmd, stdout, stderr, killCh, processExit)
 				})
 
-				go func() {
-					_, _ = io.Copy(os.Stdout, stdout)
-					_, _ = cmd.Process.Wait()
-				}()
+				go copyOutput(os.Stdout, stdout)
+				go copyOutput(os.Stderr, stderr)
 
-				go func() {
-					_, _ = io.Copy(os.Stderr, stderr)
-					_, _ = cmd.Process.Wait()
-				}()
 				state, _ := cmd.Process.Wait()
 				close(processExit)
 
diff --git a/runner/util.go b/runner/util.go
index 79700b9..63c8e88 100644
--- a/runner/util.go
+++ b/runner/util.go
@@ -1,10 +1,12 @@
 package runner
 
 import (
+	"bufio"
 	"crypto/sha256"
 	"encoding/hex"
 	"errors"
 	"fmt"
+	"io"
 	"log"
 	"os"
 	"path/filepath"
@@ -97,6 +99,24 @@ func cleanPath(path string) string {
 	return strings.TrimSuffix(strings.TrimSpace(path), "/")
 }
 
+func isSubPath(base, target string) bool {
+	if base == "" || target == "" {
+		return false
+	}
+	base = filepath.Clean(base)
+	target = filepath.Clean(target)
+	rel, err := filepath.Rel(base, target)
+	if err != nil {
+		return false
+	}
+	if rel == "." {
+		return true
+	}
+	rel = filepath.Clean(rel)
+	prefix := ".." + string(os.PathSeparator)
+	return rel != ".." && !strings.HasPrefix(rel, prefix)
+}
+
 func (e *Engine) isExcludeDir(path string) bool {
 	cleanName := cleanPath(e.config.rel(path))
 	for _, d := range e.config.Build.ExcludeDir {
@@ -109,23 +129,23 @@ func (e *Engine) isExcludeDir(path string) bool {
 
 // return isIncludeDir, walkDir
 func (e *Engine) checkIncludeDir(path string) (bool, bool) {
-	cleanName := cleanPath(e.config.rel(path))
-	iDir := e.config.Build.IncludeDir
-	if len(iDir) == 0 { // ignore empty
+	path = filepath.Clean(path)
+
+	if len(e.config.Build.includeDirAbs) == 0 {
 		return true, true
 	}
-	if cleanName == "." {
-		return false, true
+
+	if !isSubPath(e.config.Root, path) {
+		return true, true
 	}
+
 	walkDir := false
-	for _, d := range iDir {
-		if d == cleanName {
+	for _, dir := range e.config.Build.includeDirAbs {
+		dir = filepath.Clean(dir)
+		if isSubPath(dir, path) {
 			return true, true
 		}
-		if strings.HasPrefix(cleanName, d) { // current dir is sub-directory of `d`
-			return true, true
-		}
-		if strings.HasPrefix(d, cleanName) { // `d` is sub-directory of current dir
+		if isSubPath(path, dir) {
 			walkDir = true
 		}
 	}
@@ -207,6 +227,13 @@ func (e *Engine) logWithLock(f func()) {
 	e.ll.Unlock()
 }
 
+func copyOutput(dst io.Writer, src io.Reader) {
+	scanner := bufio.NewScanner(src)
+	for scanner.Scan() {
+		_, _ = dst.Write([]byte(scanner.Text() + "\n"))
+	}
+}
+
 func expandPath(path string) (string, error) {
 	if strings.HasPrefix(path, "~/") {
 		home := os.Getenv("HOME")
@@ -343,11 +370,17 @@ func setValue2Struct(v reflect.Value, fieldName string, value string) {
 		case reflect.String:
 			field.SetString(value)
 		case reflect.Slice:
+			var parts []string
 			if len(value) == 0 {
-				field.Set(reflect.ValueOf([]string{}))
+				parts = []string{}
 			} else {
-				field.Set(reflect.ValueOf(strings.Split(value, sliceCmdArgSeparator)))
+				parts = strings.Split(value, sliceCmdArgSeparator)
+			}
+			slice := reflect.MakeSlice(field.Type(), len(parts), len(parts))
+			for i, part := range parts {
+				slice.Index(i).Set(reflect.ValueOf(part))
 			}
+			field.Set(slice)
 		case reflect.Int64:
 			i, _ := strconv.ParseInt(value, 10, 64)
 			field.SetInt(i)
diff --git a/runner/util_linux.go b/runner/util_linux.go
index 26061d8..cf01fd7 100644
--- a/runner/util_linux.go
+++ b/runner/util_linux.go
@@ -1,12 +1,16 @@
 package runner
 
 import (
+	"errors"
+	"fmt"
 	"io"
+	"os"
 	"os/exec"
+	"strconv"
+	"strings"
+	"sync"
 	"syscall"
 	"time"
-
-	"github.com/creack/pty"
 )
 
 func (e *Engine) killCmd(cmd *exec.Cmd) (pid int, err error) {
@@ -14,14 +18,14 @@ func (e *Engine) killCmd(cmd *exec.Cmd) (pid int, err error) {
 
 	if e.config.Build.SendInterrupt {
 		// Sending a signal to make it clear to the process that it is time to turn off
-		if err = syscall.Kill(-pid, syscall.SIGINT); err != nil {
+		if err = sendSignalToProcessTree(pid, syscall.SIGINT); err != nil {
 			return
 		}
 		time.Sleep(e.config.killDelay())
 	}
 
 	// https://stackoverflow.com/questions/22470193/why-wont-go-kill-a-child-process-correctly
-	err = syscall.Kill(-pid, syscall.SIGKILL)
+	err = sendSignalToProcessTree(pid, syscall.SIGKILL)
 
 	// Wait releases any resources associated with the Process.
 	_, _ = cmd.Process.Wait()
@@ -30,9 +34,118 @@ func (e *Engine) killCmd(cmd *exec.Cmd) (pid int, err error) {
 
 func (e *Engine) startCmd(cmd string) (*exec.Cmd, io.ReadCloser, io.ReadCloser, error) {
 	c := exec.Command("/bin/sh", "-c", cmd)
-	f, err := pty.Start(c)
+	// Set Setpgid to create a new process group (not possible when using pty)
+	c.SysProcAttr = &syscall.SysProcAttr{
+		Setpgid: true,
+	}
+
+	stderr, err := c.StderrPipe()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+	stdout, err := c.StdoutPipe()
+	if err != nil {
+		return nil, nil, nil, err
+	}
+
+	c.Stdout = os.Stdout
+	c.Stderr = os.Stderr
+
+	err = c.Start()
 	if err != nil {
 		return nil, nil, nil, err
 	}
-	return c, f, f, nil
+	return c, stdout, stderr, nil
+}
+
+func sendSignalToProcessTree(pid int, sig syscall.Signal) error {
+	descendants, descendantsErr := collectDescendantPIDs(pid)
+	var errs []error
+
+	if descendantsErr != nil && !errors.Is(descendantsErr, os.ErrNotExist) {
+		errs = append(errs, descendantsErr)
+	}
+
+	// Try to signal the whole process group first.
+	groupErr := syscall.Kill(-pid, sig)
+	if groupErr != nil && !errors.Is(groupErr, syscall.EPERM) && !errors.Is(groupErr, syscall.ESRCH) {
+		errs = append(errs, groupErr)
+	}
+
+	// Always signal the root pid as well in case it moved to another process group.
+	procErr := syscall.Kill(pid, sig)
+	if procErr != nil && !errors.Is(procErr, syscall.ESRCH) {
+		errs = append(errs, procErr)
+	}
+
+	// Send signals to descendants concurrently for better performance
+	var wg sync.WaitGroup
+	var mu sync.Mutex
+	for _, child := range descendants {
+		wg.Add(1)
+		go func(childPID int) {
+			defer wg.Done()
+			if err := syscall.Kill(childPID, sig); err != nil && !errors.Is(err, syscall.ESRCH) {
+				mu.Lock()
+				errs = append(errs, err)
+				mu.Unlock()
+			}
+		}(child)
+	}
+	wg.Wait()
+
+	if len(errs) == 0 && errors.Is(groupErr, syscall.ESRCH) && errors.Is(procErr, syscall.ESRCH) {
+		return syscall.ESRCH
+	}
+
+	return errors.Join(errs...)
+}
+
+func collectDescendantPIDs(pid int) ([]int, error) {
+	children, err := readChildPIDs(pid)
+	if err != nil {
+		if errors.Is(err, os.ErrNotExist) {
+			return nil, nil
+		}
+		return nil, err
+	}
+
+	var (
+		allChildren = make([]int, 0, len(children))
+		errs        []error
+	)
+	for _, child := range children {
+		allChildren = append(allChildren, child)
+		descendants, err := collectDescendantPIDs(child)
+		if err != nil {
+			if errors.Is(err, os.ErrNotExist) {
+				continue
+			}
+			errs = append(errs, err)
+			continue
+		}
+		allChildren = append(allChildren, descendants...)
+	}
+
+	return allChildren, errors.Join(errs...)
+}
+
+func readChildPIDs(pid int) ([]int, error) {
+	path := fmt.Sprintf("/proc/%d/task/%d/children", pid, pid)
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+
+	fields := strings.Fields(string(data))
+	children := make([]int, 0, len(fields))
+	for _, field := range fields {
+		childPID, err := strconv.Atoi(field)
+		if err != nil {
+			continue
+		}
+		children = append(children, childPID)
+	}
+
+	return children, nil
 }



Test patch:
diff --git a/runner/_testdata/both/.air.conf b/runner/_testdata/both/.air.conf
deleted file mode 100644
index c7b9d0c..0000000
--- a/runner/_testdata/both/.air.conf
+++ /dev/null
@@ -1 +0,0 @@
-root = "invalid"
diff --git a/runner/_testdata/both/.air.toml b/runner/_testdata/both/.air.toml
index bd3525e..581c1be 100644
--- a/runner/_testdata/both/.air.toml
+++ b/runner/_testdata/both/.air.toml
@@ -1 +1,3 @@
+#:schema https://json.schemastore.org/any.json
+
 root = "both_root"
diff --git a/runner/_testdata/conf/.air.conf b/runner/_testdata/conf/.air.conf
deleted file mode 100644
index ce37422..0000000
--- a/runner/_testdata/conf/.air.conf
+++ /dev/null
@@ -1 +0,0 @@
-root = "conf_root"
diff --git a/runner/_testdata/run-detached-process.sh b/runner/_testdata/run-detached-process.sh
new file mode 100755
index 0000000..9e01738
--- /dev/null
+++ b/runner/_testdata/run-detached-process.sh
@@ -0,0 +1,8 @@
+#!/bin/sh
+echo "ProcessID=$$ begins ($0)"
+echo "$$" >> pid
+setsid sh -c './_testdata/grandchild.sh detached' >/dev/null 2>&1 &
+DETACHED_PID=$!
+echo "$DETACHED_PID" >> pid
+sleep 9999
+echo "ProcessID=$$ ends ($0)"
diff --git a/runner/_testdata/toml/.air.toml b/runner/_testdata/toml/.air.toml
index 7dee607..918ef31 100644
--- a/runner/_testdata/toml/.air.toml
+++ b/runner/_testdata/toml/.air.toml
@@ -1 +1,3 @@
+#:schema https://json.schemastore.org/any.json
+
 root = "toml_root"
diff --git a/runner/config_test.go b/runner/config_test.go
index fa583eb..3595595 100644
--- a/runner/config_test.go
+++ b/runner/config_test.go
@@ -2,7 +2,10 @@ package runner
 
 import (
 	"flag"
+	"io"
 	"os"
+	"path/filepath"
+	"reflect"
 	"runtime"
 	"strings"
 	"testing"
@@ -82,14 +85,6 @@ func TestDefaultPathConfig(t *testing.T) {
 		name: "TOML",
 		path: "_testdata/toml",
 		root: "toml_root",
-	}, {
-		name: "Conf",
-		path: "_testdata/conf",
-		root: "conf_root",
-	}, {
-		name: "Both",
-		path: "_testdata/both",
-		root: "both_root",
 	}}
 
 	for _, tt := range tests {
@@ -123,12 +118,96 @@ func TestConfPreprocess(t *testing.T) {
 		t.Fatalf("preprocess error %v", err)
 	}
 	suffix := "/_testdata/toml/tmp/main"
-	binPath := df.Build.Bin
+	binPath := df.Build.Entrypoint.binary()
 	if !strings.HasSuffix(binPath, suffix) {
 		t.Fatalf("bin path is %s, but not have suffix  %s.", binPath, suffix)
 	}
 }
 
+func TestEntrypointResolvesAbsolutePath(t *testing.T) {
+	base := t.TempDir()
+	rootWithSpace := filepath.Join(base, "with space")
+	if err := os.MkdirAll(filepath.Join(rootWithSpace, "tmp"), 0o755); err != nil {
+		t.Fatalf("failed to prepare tmp dir: %v", err)
+	}
+
+	cfg := defaultConfig()
+	cfg.Root = rootWithSpace
+	cfg.Build.Entrypoint = entrypoint{"./tmp/main"}
+
+	if err := cfg.preprocess(nil); err != nil {
+		t.Fatalf("preprocess error %v", err)
+	}
+
+	want := filepath.Join(rootWithSpace, "tmp", "main")
+	if got := cfg.Build.Entrypoint.binary(); got != want {
+		t.Fatalf("entrypoint is %s, but want %s", got, want)
+	}
+
+	if cfg.binPath() != want {
+		t.Fatalf("bin path is %s, but want %s", cfg.binPath(), want)
+	}
+}
+
+func TestEntrypointResolvesFromPath(t *testing.T) {
+	root := t.TempDir()
+	pathDir := t.TempDir()
+
+	binName := "air-entrypoint-path"
+	fileName := binName
+	fileContents := "#!/bin/sh\nexit 0\n"
+	if runtime.GOOS == "windows" {
+		fileName += ".bat"
+		fileContents = "@echo off\r\n"
+		t.Setenv("PATHEXT", ".BAT;.EXE")
+	}
+	fullPath := filepath.Join(pathDir, fileName)
+	if err := os.WriteFile(fullPath, []byte(fileContents), 0o755); err != nil {
+		t.Fatalf("failed to write fake binary: %v", err)
+	}
+	if runtime.GOOS != "windows" {
+		if err := os.Chmod(fullPath, 0o755); err != nil {
+			t.Fatalf("failed to make fake binary executable: %v", err)
+		}
+	}
+
+	t.Setenv("PATH", pathDir+string(os.PathListSeparator)+os.Getenv("PATH"))
+
+	cfg := defaultConfig()
+	cfg.Root = root
+	cfg.Build.Entrypoint = entrypoint{binName}
+
+	if err := cfg.preprocess(nil); err != nil {
+		t.Fatalf("preprocess error %v", err)
+	}
+
+	want := fullPath
+	if got := cfg.Build.Entrypoint.binary(); got != want {
+		t.Fatalf("entrypoint resolved to %s, want %s", got, want)
+	}
+}
+
+func TestEntrypointPreservesArgs(t *testing.T) {
+	root := t.TempDir()
+	cfg := defaultConfig()
+	cfg.Root = root
+	cfg.Build.Entrypoint = entrypoint{"./tmp/main", "server", ":8080"}
+
+	if err := cfg.preprocess(nil); err != nil {
+		t.Fatalf("preprocess error %v", err)
+	}
+
+	wantBin := filepath.Join(root, "tmp", "main")
+	if cfg.Build.Entrypoint.binary() != wantBin {
+		t.Fatalf("entrypoint binary is %s, want %s", cfg.Build.Entrypoint.binary(), wantBin)
+	}
+
+	wantArgs := []string{"server", ":8080"}
+	if got := cfg.Build.Entrypoint.args(); !reflect.DeepEqual(got, wantArgs) {
+		t.Fatalf("entrypoint args mismatch, got %v want %v", got, wantArgs)
+	}
+}
+
 func TestConfigWithRuntimeArgs(t *testing.T) {
 	runtimeArg := "-flag=value"
 
@@ -210,3 +289,39 @@ func contains(sl []string, target string) bool {
 	}
 	return false
 }
+
+func TestWarnDeprecatedBin(t *testing.T) {
+	tmpDir := t.TempDir()
+	cfgPath := filepath.Join(tmpDir, ".air.toml")
+	cfgContent := `
+[build]
+bin = "./tmp/main"
+cmd = "go build -o ./tmp/main ."
+`
+	if err := os.WriteFile(cfgPath, []byte(cfgContent), 0o644); err != nil {
+		t.Fatalf("failed to write config: %v", err)
+	}
+
+	oldStdout := os.Stdout
+	r, w, err := os.Pipe()
+	if err != nil {
+		t.Fatalf("failed to create pipe: %v", err)
+	}
+	os.Stdout = w
+
+	_, _ = InitConfig(cfgPath, nil)
+
+	if err := w.Close(); err != nil {
+		t.Fatalf("failed to close writer: %v", err)
+	}
+	os.Stdout = oldStdout
+
+	out, err := io.ReadAll(r)
+	if err != nil {
+		t.Fatalf("failed to read output: %v", err)
+	}
+	output := string(out)
+	if !strings.Contains(output, "build.bin is deprecated") {
+		t.Fatalf("missing bin deprecation warning in output: %q", output)
+	}
+}
diff --git a/runner/engine_test.go b/runner/engine_test.go
index c8998cc..d6bdf49 100644
--- a/runner/engine_test.go
+++ b/runner/engine_test.go
@@ -203,10 +203,10 @@ func TestRunBin(t *testing.T) {
 
 func GetPort() (int, func()) {
 	l, err := net.Listen("tcp", ":0")
-	port := l.Addr().(*net.TCPAddr).Port
 	if err != nil {
 		panic(err)
 	}
+	port := l.Addr().(*net.TCPAddr).Port
 	return port, func() {
 		_ = l.Close()
 	}
@@ -423,6 +423,9 @@ func TestFixCloseOfChannelAfterCtrlC(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Should not be fail: %s.", err)
 	}
+	// Silence engine logs to keep this test output readable.
+	engine.config.Log.Silent = true
+	silenceBuildCmd(engine.config)
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 	go func() {
@@ -468,6 +471,8 @@ func TestFixCloseOfChannelAfterTwoFailedBuild(t *testing.T) {
 	if err != nil {
 		t.Fatalf("Should not be fail: %s.", err)
 	}
+	engine.config.Log.Silent = true
+	silenceBuildCmd(engine.config)
 	sigs := make(chan os.Signal, 1)
 	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 	go func() {
@@ -708,6 +713,17 @@ go 1.17
 	return nil
 }
 
+func silenceBuildCmd(cfg *Config) {
+	if cfg == nil {
+		return
+	}
+	if runtime.GOOS == "windows" {
+		cfg.Build.Cmd = fmt.Sprintf("%s >NUL 2>&1", cfg.Build.Cmd)
+		return
+	}
+	cfg.Build.Cmd = fmt.Sprintf("%s >/dev/null 2>&1", cfg.Build.Cmd)
+}
+
 func TestRebuildWhenRunCmdUsingDLV(t *testing.T) {
 	// generate a random port
 	port, f := GetPort()
@@ -767,12 +783,8 @@ func TestRebuildWhenRunCmdUsingDLV(t *testing.T) {
 }
 
 func TestWriteDefaultConfig(t *testing.T) {
-	port, f := GetPort()
-	f()
-	t.Logf("port: %d", port)
-
-	tmpDir := initTestEnv(t, port)
-	// change dir to tmpDir
+	// work inside a clean temp directory to avoid touching real project files
+	tmpDir := t.TempDir()
 	chdir(t, tmpDir)
 	configName, err := writeDefaultConfig()
 	if err != nil {
@@ -783,12 +795,22 @@ func TestWriteDefaultConfig(t *testing.T) {
 		t.Fatal(err)
 	}
 
+	raw, err := os.ReadFile(configName)
+	if err != nil {
+		t.Fatal(err)
+	}
+	expectedPrefix := schemaHeader + "\n\n"
+	assert.True(t, strings.HasPrefix(string(raw), expectedPrefix), "config should start with schema header")
+
 	// check the file content is right
 	actual, err := readConfig(configName)
 	if err != nil {
 		t.Fatal(err)
 	}
 	expect := defaultConfig()
+	if len(expect.Build.Entrypoint) == 0 && expect.Build.Bin != "" {
+		expect.Build.Entrypoint = entrypoint{expect.Build.Bin}
+	}
 
 	assert.Equal(t, expect, *actual)
 }
diff --git a/runner/flag_test.go b/runner/flag_test.go
index 8bd535b..497d895 100644
--- a/runner/flag_test.go
+++ b/runner/flag_test.go
@@ -4,6 +4,8 @@ import (
 	"flag"
 	"log"
 	"os"
+	"path/filepath"
+	"strings"
 	"testing"
 	"time"
 
@@ -139,6 +141,14 @@ func TestConfigRuntimeArgs(t *testing.T) {
 				assert.False(t, patterns[0].MatchString("other_file.go"), "regex shouldn't match other_file.go")
 			},
 		},
+		{
+			name: "check entrypoint flag",
+			args: []string{"--build.entrypoint", "./tmp/server"},
+			check: func(t *testing.T, conf *Config) {
+				want := filepath.Join("tmp", "server")
+				assert.True(t, strings.HasSuffix(conf.Build.Entrypoint.binary(), want), "entrypoint %s does not end with %s", conf.Build.Entrypoint.binary(), want)
+			},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
diff --git a/runner/util_linux_test.go b/runner/util_linux_test.go
new file mode 100644
index 0000000..985e69b
--- /dev/null
+++ b/runner/util_linux_test.go
@@ -0,0 +1,85 @@
+package runner
+
+import (
+	"errors"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strconv"
+	"strings"
+	"syscall"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func Test_sendSignalToProcessTree_ConcurrentSignalSending(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skip("requires /proc")
+	}
+
+	_, b, _, _ := runtime.Caller(0)
+	dir := filepath.Dir(b)
+	err := os.Chdir(dir)
+	if err != nil {
+		t.Fatalf("couldn't change directory: %v", err)
+	}
+
+	_ = os.Remove("pid")
+	defer os.Remove("pid")
+
+	e := Engine{
+		config: &Config{
+			Build: cfgBuild{
+				SendInterrupt: false,
+			},
+		},
+	}
+
+	// Start a process tree with multiple children
+	startChan := make(chan *exec.Cmd)
+	go func() {
+		cmd, _, _, err := e.startCmd("sh _testdata/run-many-processes.sh")
+		if err != nil {
+			t.Errorf("failed to start command: %v", err)
+			return
+		}
+		startChan <- cmd
+		if err := cmd.Wait(); err != nil {
+			t.Logf("wait returned: %v", err)
+		}
+	}()
+
+	cmd := <-startChan
+	pid := cmd.Process.Pid
+	time.Sleep(2 * time.Second)
+
+	// Send signal using the concurrent implementation
+	err = sendSignalToProcessTree(pid, syscall.SIGKILL)
+
+	// Should not return an error for successful kill
+	if err != nil && !errors.Is(err, syscall.ESRCH) {
+		t.Errorf("unexpected error from sendSignalToProcessTree: %v", err)
+	}
+
+	// Verify all processes were killed
+	bytesRead, err := os.ReadFile("pid")
+	require.NoError(t, err)
+	lines := strings.Split(string(bytesRead), "\n")
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		if _, err := strconv.Atoi(line); err != nil {
+			t.Logf("failed to convert str to int %v", err)
+			continue
+		}
+		_, err = exec.Command("ps", "-p", line, "-o", "comm= ").Output()
+		if err == nil {
+			t.Fatalf("process should be killed %v", line)
+		}
+	}
+}
diff --git a/runner/util_test.go b/runner/util_test.go
index b526f05..6032185 100644
--- a/runner/util_test.go
+++ b/runner/util_test.go
@@ -57,6 +57,53 @@ func TestExpandPathWithHomePath(t *testing.T) {
 	}
 }
 
+func TestNormalizeIncludeDirOutsideRoot(t *testing.T) {
+	root := t.TempDir()
+	parent := filepath.Dir(root)
+	external := filepath.Join(parent, "pkg")
+
+	cfg := &Config{
+		Root: root,
+		Build: cfgBuild{
+			IncludeDir: []string{"../pkg"},
+		},
+	}
+	cfg.Build.normalizeIncludeDirs(cfg.Root)
+
+	require.Empty(t, cfg.Build.includeDirAbs)
+	require.Equal(t, []string{filepath.Clean(external)}, cfg.Build.extraIncludeDirs)
+
+	engine := &Engine{config: cfg}
+	isIn, walk := engine.checkIncludeDir(filepath.Join(root, "runner"))
+	require.True(t, isIn)
+	require.True(t, walk)
+}
+
+func TestCheckIncludeDirRestrictsWithinRoot(t *testing.T) {
+	root := t.TempDir()
+	runnerDir := filepath.Join(root, "runner")
+	require.NoError(t, os.Mkdir(runnerDir, 0o755))
+	otherDir := filepath.Join(root, "other")
+	require.NoError(t, os.Mkdir(otherDir, 0o755))
+
+	cfg := &Config{
+		Root: root,
+		Build: cfgBuild{
+			IncludeDir: []string{"runner"},
+		},
+	}
+	cfg.Build.normalizeIncludeDirs(cfg.Root)
+
+	engine := &Engine{config: cfg}
+	isIn, walk := engine.checkIncludeDir(runnerDir)
+	require.True(t, isIn)
+	require.True(t, walk)
+
+	isIn, walk = engine.checkIncludeDir(otherDir)
+	require.False(t, isIn)
+	require.False(t, walk)
+}
+
 func TestFileChecksum(t *testing.T) {
 	tests := []struct {
 		name                  string
@@ -235,6 +282,68 @@ func Test_killCmd_SendInterrupt_false(t *testing.T) {
 	}
 }
 
+func Test_killCmd_KillsDetachedChildren(t *testing.T) {
+	if runtime.GOOS != "linux" {
+		t.Skip("requires /proc")
+	}
+
+	_, b, _, _ := runtime.Caller(0)
+	dir := filepath.Dir(b)
+	err := os.Chdir(dir)
+	if err != nil {
+		t.Fatalf("couldn't change directory: %v", err)
+	}
+
+	_ = os.Remove("pid")
+	defer os.Remove("pid")
+
+	e := Engine{
+		config: &Config{
+			Build: cfgBuild{
+				SendInterrupt: false,
+			},
+		},
+	}
+
+	startChan := make(chan *exec.Cmd)
+	go func() {
+		cmd, _, _, err := e.startCmd("sh _testdata/run-detached-process.sh")
+		if err != nil {
+			t.Errorf("failed to start command: %v", err)
+			return
+		}
+		startChan <- cmd
+		if err := cmd.Wait(); err != nil {
+			t.Logf("failed to wait command: %v", err)
+		}
+	}()
+
+	cmd := <-startChan
+	time.Sleep(2 * time.Second)
+
+	if _, err := e.killCmd(cmd); err != nil {
+		t.Fatalf("failed to kill command: %v", err)
+	}
+
+	bytesRead, err := os.ReadFile("pid")
+	require.NoError(t, err)
+	lines := strings.Split(string(bytesRead), "\n")
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		if _, err := strconv.Atoi(line); err != nil {
+			t.Logf("failed to convert str to int %v", err)
+			continue
+		}
+		_, err = exec.Command("ps", "-p", line, "-o", "comm= ").Output()
+		if err == nil {
+			t.Fatalf("process should be killed %v", line)
+		}
+	}
+}
+
 func TestGetStructureFieldTagMap(t *testing.T) {
 	c := Config{}
 	tagMap := flatConfig(c)