Use one executable ~/.tmux.services to manage services across projects; each project only maintains its own foreground launcher.
~/.tmux.services # 全局清单与 tmux 生命周期
~/dev/project-a/tools/start_ocr_server.sh
~/dev/project-b/tools/start_chat_server.sh
When the service name is globally unique, directly use <name>, with session as serv-<name>; only when there is a name conflict use <project>/<name>, with session as serv-<project>-<name>。
Dependencies
Requires Bash 3.2+ and tmux:
brew install tmux # macOS
# apt install tmux # Debian / Ubuntu
Install the global script
First record this skill's absolute directory as SKILL_ROOT:
SKILL_ROOT="<tmux-serv skill 的绝对路径>"
bash "$SKILL_ROOT/scripts/install.sh"
On first install, write ~/.tmux.services and set it to 0700。If the target already exists, the installer refuses to overwrite。
For isolated/test scenarios, you can specify a custom target path, without writing to ~/.tmux.services; if the target already exists, it likewise refuses to overwrite:
bash "$SKILL_ROOT/scripts/install.sh" --target /path/to/.tmux.services
Upgrade template:
bash "$SKILL_ROOT/scripts/install.sh" --upgrade
Upgrading keeps only the user service section between the following markers, replaces the rest of the engine with the new version, and first creates a timestamped .bak backup:
# >>> tmux-serv user services >>>
# <<< tmux-serv user services <<<
If markers are missing, duplicated, or the merged result fails validation, the installer refuses to modify. The installer only cleans up its own temporary merge files and does not delete user configuration or backups. Do not manually modify the engine outside the markers; complex customizations should keep their own fork.
Project launcher contract
One in-project script per service, placed directly in the project root's tools/ :
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"
exec env PORT="${API_PORT:-3000}" node server.js
Python service example:
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"
# 优先用项目内 venv,没有则回落到系统 python
if [[ -x ".venv/bin/python" ]]; then
PY=".venv/bin/python"
else
PY="python3"
fi
exec env \
PORT="${API_PORT:-8000}" \
HOST="${API_HOST:-127.0.0.1}" \
"$PY" -m app.main
If it is a uv-managed project, it can be simplified to:
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT_DIR"
exec env PORT="${API_PORT:-8000}" uv run python -m app.main
Must satisfy:
- The file is located at
<project>/tools/, exists, and is executable - Locate the project from its own path, not depending on the caller's cwd
- Do not create tmux, do not use
nohup, trailing&, or self-daemonize - Ultimately
execthe foreground main process; dependencies, ports, parameters, and environment defaults are maintained by the project - On startup failure, write to stderr and return non-zero; the script should be directly executable from any cwd
- Do not write secrets into
~/.tmux.services; provide them via the project's secure configuration or process environment
Register a service
Only edit the user service section of ~/.tmux.services :
configure_services() {
start_service "project-a" "ocr" \
"$HOME/dev/project-a/tools/start_ocr_server.sh"
start_service "project-b" "chat-room" \
"$HOME/dev/project-b/tools/start_chat_server.sh"
}
start_service receives exactly three parameters:
| Parameter | Rule |
|---|---|
project |
1–40 lowercase letters, digits, single hyphens; leading/trailing hyphens and -- |
service |
Same as above; only needs to be unique within the project |
launcher |
Absolute path or ~/ path; spaces allowed, must be directly located in the project tools/ |
project/service The composite key must be unique. If service is unique in the entire table, the CLI directly uses service, with session as serv-<name>; if there is a name conflict, then project/servicemust be used, with session as serv-<project>-<name>. Adding a second service with the same name will change the derived session of the original service; the old serv-<name> will not be stopped automatically; you should first confirm ownership and stop precisely, then start the new session item by item. The registry is executable Shell; only install and edit content you trust; calls including --check、--status will execute the configuration section to load entries, but only startup or restart executes the launcher.
After registration, validate first without starting the service:
~/.tmux.services --check
Daily operations
# 仅补启动全部缺失服务;已有 session 不动
~/.tmux.services
# service 唯一时直接使用短名
~/.tmux.services chat-room
~/.tmux.services --restart chat-room
# 只有重名时才带项目名
~/.tmux.services project-a/api
~/.tmux.services --restart project-a/api
# 明确要求时才重启全部;非事务操作,失败不会回滚前项
~/.tmux.services --restart-all
# 查看全部或单项状态
~/.tmux.services --status
~/.tmux.services --status chat-room
# 最近 200 行、附着、精确停止
~/.tmux.services --logs chat-room
~/.tmux.services --attach chat-room
~/.tmux.services --stop chat-room
All existence checks, stops, and operation targets use tmux's =<session> Exact match. If the session disappears within the 1-second stabilization window after startup, the command returns 70 and prompts you to run the launcher directly for troubleshooting; this is not a port or HTTP health check.
Safe default
- With no arguments, only fills in missing services at startup; does not restart existing services
- Only an explicit
--restart-alltriggers a full restart - Does not
sourceexternal configuration, does notevalservice commands; the main script directly executes the registered launcher - The launcher path is physically normalized; symlinks, relative paths, and
tools/external entries are rejected --statusOnly lists registered services; does not scan or operate on otherserv-session- Test-only
TMUX_SERV_SOCKET_NAMEcan switch to an independent tmux server; do not set it for daily use. This variable must be set before calling~/.tmux.servicesbeforeexport; writing it to the config file has no effect; alltmuxacceptance commands must include-L "$TMUX_SERV_SOCKET_NAME"to hit the isolated server
Exit code
| Exit code | Meaning |
|---|---|
0 |
Operation completed, including "already running/not running so skipped" |
64 |
Argument, name, or registration configuration error |
66 |
launcher missing, not executable, or path non-compliant |
69 |
tmux missing, or log/attach target not running |
70 |
tmux start/stop failed, or the service exited immediately after startup |
--restart-all Executes item by item and is not transactional; if earlier items succeed and later items fail, there is no rollback.
Acceptance
bash -n ~/.tmux.services
~/.tmux.services --check
~/.tmux.services <name>
tmux has-session -t '=serv-<name>'
tmux display-message -p -t '=serv-<name>:' '#{pane_current_path}'
~/.tmux.services --status <name>
# 仅重名服务改用 project/service 与 serv-<project>-<name>
~/.tmux.services <project>/<name>
tmux has-session -t '=serv-<project>-<name>'
Isolated test
Test-only; no need to set for daily use
export TMUX_SERV_SOCKET_NAME=tmuxserv-sandbox
~/.tmux.services <name>
tmux -L "$TMUX_SERV_SOCKET_NAME" has-session -t '=serv-<name>'
tmux -L "$TMUX_SERV_SOCKET_NAME" display-message -p -t '=serv-<name>:' '#{pane_current_path}'
~/.tmux.services --status <name>
unset TMUX_SERV_SOCKET_NAME
Then check ports, health endpoints, and logs according to the service protocol.