Как запускать параллельных coding-агентов и не ловить EADDRINUSE
Git worktree разделяет файлы, но не сетевые ресурсы. Когда агенты поднимают разрабатываемое приложение для локальной проверки и e2e-тестов, его серверы могут попытаться слушать один и тот же порт.

Зачем агентам отдельные worktree
git worktree создаёт дополнительную рабочую директорию того же репозитория, в которой можно checkout-ить другую ветку. Git-объекты и история остаются общими, но у каждого worktree свои файлы и индекс Git.
Это нужно, когда несколько агентов или разработчиков одновременно выполняют независимые задачи. В одной рабочей директории их незакоммиченные изменения, область подготовки к коммиту и запуск тестов смешиваются: один агент может проверить полуготовый код другого или закоммитить чужие файлы. Отдельный worktree даёт каждой задаче и ветке собственную директорию; конфликты всё равно возможны, но проявляются при осознанном merge, а не во время работы.
Worktree не заменяет декомпозицию: если агенты меняют одну часть системы, смысловой конфликт останется. Но он позволяет вести независимые потоки без постоянного stash и checkout. Практический пример параллельных агентных сессий — у incident.io.
Когда у меня одновременно работают несколько coding-агентов, каждый получает свой git worktree. Один меняет авторизацию, второй правит интерфейс, третий гоняет end-to-end тесты. На уровне Git они изолированы. На уровне операционной системы все они по-прежнему делят один localhost.
Из-за этого агент, который запускает очередной Vite, Rails, Next.js или Playwright web server, может получить:
Error: listen EADDRINUSE: address already in use :::3000
Сама ошибка несложная. Проблема в её операционной цене: агент отвлекается от задачи, ищет процесс, решает, можно ли его убить, меняет порт в одной команде и забывает передать его тестам. В худшем случае он останавливает чужой сервис.
Почему случайный порт не решает всю задачу
Первая идея — попросить операционную систему выдать любой свободный порт. Для одного процесса этого часто достаточно. Но в agentic workflow порт нужен не одной строке.
Его должны знать:
- команда запуска dev server;
- browser-тесты и их
BASE_URL; - человек, открывающий preview;
- следующая сессия агента;
- иногда другие сервисы того же worktree.
Анонимный порт хорош внутри одного процесса. Для координации нужен адрес, который можно повторно получить по имени проекта.
Какой контракт нужен
Я свёл задачу к четырём свойствам.
1. Порт свободен в момент выдачи
Аллокатор не просто инкрементирует число. Он проверяет, что порт не занят процессом. Это не устраняет все гонки: между выбором порта и bind (моментом, когда сервер его занимает) всё ещё есть короткое окно. Поэтому команду запуска лучше строить как одну операцию.
2. Порт стабилен для директории
Один и тот же worktree должен получать тот же порт. Тогда preview URL не меняется после каждого перезапуска, а тесты и агенты могут повторно найти его без копирования числа в промпт.
3. Одна директория может иметь несколько адресов
Монорепозиторий может поднимать frontend, API и локальную базу. Поэтому ключ аллокации — не только директория, но пара (directory, name).
4. Параллельные вызовы не портят реестр
Два агента могут запросить порт одновременно. Запись о выделении должна быть сериализована. В port-selector для этого используется file locking на Unix. На Windows сборка есть, но этой гарантии пока нет — это важное ограничение, а не мелкая сноска.
Установка
На macOS проще всего установить утилиту через Homebrew:
brew tap dapi/tapbrew install port-selector
Проверка:
port-selector3000
Повторный вызов из той же директории вернёт тот же порт. Из другого worktree — другой.
Dev server
PORT="$(port-selector)"npm run dev -- --port "$PORT"
Получение порта и запуск стоят рядом. Это сужает окно между проверкой и bind.
Playwright
PORT="$(port-selector --name e2e)"BASE_URL="http://127.0.0.1:$PORT" npx playwright test
Если тестовый сервер стартует в конфигурации Playwright, туда нужно передать тот же порт. Главный принцип: не выбирать его дважды.
direnv
Для worktree удобно выделять порт при входе в директорию:
.envrcexport PORT="$(port-selector --name web)"
Тогда обычная команда npm run dev может читать PORT из окружения, а агенту не нужно запоминать число.
Несколько сервисов
export WEB_PORT="$(port-selector --name web)"export API_PORT="$(port-selector --name api)"export DB_PORT="$(port-selector --name db)"
Имя — часть ключа. Повторный port-selector --name api из той же директории вернёт прежний API-порт, а не создаст новый.
Что записать в инструкции агента
Важно не просто установить CLI, а убрать из промпта выбор порта. В AGENTS.md или аналогичном файле достаточно короткого контракта:
AGENTS.md## Local servers
Before starting any process that binds localhost, allocate a stable port
from the repository root:
PORT="$(port-selector --name web)"
npm run dev -- --port "$PORT"
Выделение происходит из корня конкретной директории worktree: путь — часть идентичности.
Как подключить skill агенту
В репозитории port-selector есть skill с тем же контрактом. Он применяется, когда агент запускает локальный сервер, preview, тестовый runner или сталкивается с занятым портом.
Сначала установите CLI, затем из корня проекта подключите skill:
npx skills add dapi/port-selector --skill port-selector
Если skill нужен во всех проектах, добавьте --global. После установки начните новую сессию агента, чтобы он прочитал SKILL.md.
Что не надо делать
Не зашивать порт в каждую команду
Значение 3000 быстро размножается по package.json, Playwright config, README и промптам. После первой коллизии приходится искать все копии.
Не запрашивать отдельный порт для теста
У приложения и теста должен быть один адрес. Ошибка возникает, когда сервер получает порт с именем web, а тест отдельно запрашивает порт с именем e2e:
# Сервер слушает, например, 3001.
PORT="$(port-selector --name web)"
npm run dev -- --port "$PORT"
# Тест получает другой порт, например, 3002, и не находит приложение.
BASE_URL="http://127.0.0.1:$(port-selector --name e2e)" npx playwright test
Имя — часть идентичности аллокации, поэтому web и e2e намеренно возвращают разные порты. Выберите имя один раз и передайте уже полученный PORT всем потребителям:
PORT="$(port-selector --name web)"npm run dev -- --port "$PORT" &BASE_URL="http://127.0.0.1:$PORT" npx playwright test
Если Playwright сам запускает сервер через webServer, передайте тот же PORT в его конфигурацию, а BASE_URL соберите из этого же значения.
Границы решения
port-selector не контейнер и не прокси. Он не разделяет сети и не держит socket от имени сервиса. Он решает более узкую задачу: делает выбор локального порта повторяемым и управляемым.
На macOS и Linux этого достаточно для большинства локальных dev servers. На Windows нет file-locking гарантии. На macOS поиск владельца процесса ограничен по сравнению с Linux. Если нужна жёсткая network isolation или атомарная передача socket, нужен другой уровень изоляции.
Итог
Проблема не в числе 3000. Она в ложном ощущении полной изоляции: отдельный worktree даёт задаче собственные файлы, но локальный сервер, тесты и preview по-прежнему работают в общей сети localhost.
Без явного контракта каждый запуск заново решает, какой порт использовать. Один агент занимает 3000, другой получает EADDRINUSE, а после случайной замены порта browser-тесты и preview могут продолжить ходить по старому адресу.
port-selector не изолирует сеть и не устраняет гонку между выбором порта и bind. Он решает более узкую, но повторяющуюся проблему: один раз связывает директорию и имя сервиса со свободным портом, а сервер, тест и preview используют уже полученное значение. Поэтому агенту не нужно «разруливать порт» при каждом запуске, а несколько worktree могут параллельно проверять свои изменения на одной машине.
Ссылки
-
dapi/port-selector— исходный код, установка, ограничения и полный CLI contract. - Git worktree — каноническая документация Git о нескольких рабочих деревьях.
- Как incident.io использует Claude Code и Git worktree — практический разбор параллельных агентных сессий.
Хотите внедрить это у себя?
Помогаю командам перейти на агентную разработку: как советник, через обучение команды или внедрение изменений с проверкой эффекта по данным. Короткие заметки между статьями выходят в Telegram-канале.