timeout-dead
Lightweight command timeout utility with zero runtime dependencies.
Runs any shell command with a configurable time limit and termination signal.
If the command exceeds the timeout, timeout-dead sends the chosen signal,
waits a 1-second grace period, then force-kills the process.
Fully cross-platform — Linux, macOS, and Windows (Git Bash / WSL).
Installation
pip install timeout-dead
Or via uv:
uv tool install timeout-dead
Requires Python 3.10 or later. Zero runtime dependencies — pure Python standard library.
Quick start
# Run a command with default 60s timeout
timeout-dead "python -c 'print(42)'"
# Short alias also works
time-d "echo hello"
# Specify a custom timeout
timeout-dead --sec 120 "npm run build"
# Sub-second timeouts work too
timeout-dead --sec 0.5 "potentially-hanging-tool"
# Use SIGINT instead of default SIGTERM
timeout-dead --signal INT --sec 30 "long-running-server"
# Suppress child-process output while retaining the final exit code and status
timeout-dead --no-output "curl -s https://example.com"
# Capture stderr/stdout with a five-line live tail preview
timeout-dead --capture-output "git diff --stat"
# Run the bundled stdout/stderr demo script with live capture
time-d --capture-output "python tests/example_script.py"
Usage
usage: timeout-dead [-h] [-v] [--sec SECONDS] [--signal SIGNAL] [--no-output]
[-c] COMMAND ...
Lightweight command timeout utility.
positional arguments:
COMMAND command to execute
options:
-h, --help show this help message and exit
-v, --version show version and exit
--sec SECONDS timeout in seconds (default: 60.0, accepts floats)
--signal SIGNAL signal to send on timeout (TERM, KILL, HUP, INT)
--no-output suppress normal output and override --capture-output
-c, --capture-output capture and format stdout/stderr blocks
Default output format:
Running: git status --short --branch
Timeout: 60.0 seconds
Out:
<full command stdout/stderr>
Exit code: 0
Completed successfully
Captured final output format (--capture-output):
Running: git status --short --branch
Timeout: 60.0 seconds
Err:
<full stderr text>
Out:
<full stdout text>
Exit code: 0
Completed successfully
Exit code: <code> is always printed, including with --no-output. After a successful
command, timeout-dead prints Completed successfully; after a timeout, it prints
Timed out after <seconds> seconds. These final status messages are written to stderr and
are green for success or red for timeout when stderr is an ANSI-capable interactive terminal.
Redirected output and CI logs always receive plain text without ANSI escape sequences.
Long commands are shortened only in the Running: display line to keep the terminal readable.
Commands of up to 35 characters are shown in full. Longer commands show their first 25 and last
5 characters, separated by .... The complete command is still executed unchanged.
In capture mode, timeout-dead reads stdout and stderr concurrently while the command is
still running. When stdout is an interactive terminal, the Err: and Out: blocks are
redrawn as a live tail preview: only the last 5 stderr lines and last 5 stdout lines are
shown, so large output does not scroll the whole screen. After the command exits, the
preview is cleared and the complete stderr/stdout text is printed in the final format
shown above.
The live preview uses a fixed-height local frame and throttled redraws to reduce flicker on fast character-by-character output. The terminal cursor is hidden while the preview is active and restored when the command completes, fails, or is interrupted.
When stdout is redirected or captured by CI/test tools, output stays plain text with no
ANSI control sequences. In that mode, timeout-dead prints the same complete final blocks
after the command exits.
The default mode intentionally does not capture streams. It inherits the terminal handles
so interactive and TUI programs such as vim, less, REPLs, Gradle progress output, and
similar tools continue to behave normally. Use --capture-output only when you need
separated stdout/stderr blocks in logs or tests.
To see a longer mixed stdout/stderr example locally:
time-d --capture-output "python tests/example_script.py"
Flag priority:
-h/--helpand-v/--versionexit immediately and ignore all other arguments.--secand--signalapply whenever a command is executed.--no-outputsuppresses child-process output and start headers, has priority over--capture-output, and still prints the final exit code and status.--capture-outputis enabled only when--no-outputis not set.COMMANDis required unless-h/--helpor-v/--versionis used.
How it works
timeout-deadstarts the command in a new process group (Unix) / console group (Windows).- A background timer waits for the specified timeout.
- If the command finishes in time, its output is forwarded, followed by its exit code and a
Completed successfullystatus. - If the timeout expires:
- The chosen signal is sent to the process group.
- After 1 second, if the process is still running,
SIGKILL(Unix) orprocess.kill()(Windows) is sent. - The exit code is printed, followed by
Timed out after <seconds> secondson stderr.
Windows signal note: On Windows,
CTRL_BREAK_EVENT(used forTERMandHUP) is sent to the main process only — child processes may not receive it. They will be force-killed after the 1-second grace period. For critical cleanup, useKILLor ensure the parent handles cleanup.
Process group note: Processes that explicitly detach from their process group (e.g., via
setsidornohup) may not be terminated. EvenKILLmay not reach processes that have created a new session.
Cross-platform
Zero code changes between platforms. timeout-dead detects the OS at startup and
uses the native termination strategy:
| OS | Process isolation | Graceful signal | Force kill |
|---|---|---|---|
| Linux | setsid() process group |
SIGTERM |
SIGKILL via killpg() |
| macOS | setsid() process group |
SIGTERM |
SIGKILL via killpg() |
| Windows | Job Object (kernel32.dll) |
CTRL_BREAK_EVENT |
taskkill /T /F + Job close |
Tested on all three platforms in CI.
Signal reference
| Signal | Unix | Windows |
|---|---|---|
TERM |
SIGTERM (15) — terminate gracefully |
CTRL_BREAK_EVENT — console break |
KILL |
SIGKILL (9) — force kill |
Falls back to TerminateProcess |
HUP |
SIGHUP (1) — hangup |
Falls back to TerminateProcess |
INT |
SIGINT (2) — interrupt (Ctrl+C) |
CTRL_C_EVENT — console interrupt |
Why subprocess timeout is not enough
Python's built-in subprocess timeout only kills the direct child, not its entire process tree. If your command spawns subprocesses (npm install, make, docker build), children survive the parent kill.
timeout-dead uses process groups to terminate everything — every subprocess, pipeline, and child.
Real-world scenarios
| Scenario | Command | Why it hangs | timeout-dead |
|---|---|---|---|
| Gradle build | ./gradlew build |
20+ min fresh build, agent generates duplicate commands | timeout-dead --sec 600 "./gradlew build" |
| CMake build | cmake --build . |
Locks waiting for dependency resolution | timeout-dead --sec 180 "cmake --build ." |
| Spring Boot | ./gradlew bootRun |
Server never exits, agent won't proceed | timeout-dead --sec 30 --signal INT "./gradlew bootRun" |
| Docker build | docker build -t myapp . |
Network timeout, internal retries, no progress | timeout-dead --sec 600 "docker build -t myapp ." |
| npm install | npm install |
Corrupted cache or registry auth hang | timeout-dead --sec 300 "npm install" |
| Interactive REPL | python / node / irb |
Waits for input, agent doesn't know | timeout-dead --sec 5 "python" |
| Interactive REPL | vim / nano / less |
May leave terminal echo disabled after kill | timeout-dead --sec 10 --signal INT "vim" |
For AI agents
If you build agents that execute shell commands, timeout-dead is essential infrastructure. Agents frequently generate commands that hang — waiting for input, entering infinite loops, or starting interactive programs.
For a recorded example of an agent recovering from a failed service-readiness check, see
AI Agent Recovery Example. The session used timeout-dead 0.4.4 and preserves
its original agent commentary and command output.
Instead of agents freezing indefinitely, wrap every command:
timeout-dead --sec <timeout> --signal <signal> "<command>"
No dependencies, no code changes. Agent always gets a response — exit code + output — and can implement retry, fallback, or user notification.
Development
git clone https://github.com/UmbrellaLeaf5/timeout-dead
cd timeout-dead
uv sync --extra dev
uv run pytest tests/ -v
uv run ruff check src/timeout_dead/ tests/
uv run ruff format --check .
uv run pyright src/timeout_dead/
License
Unlicense — public domain.
Metadata
Release files for timeout-dead 0.5.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| timeout_dead-0.5.1.tar.gz | 247.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| timeout_dead-0.5.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 269.8 kB
Release files / timeout_dead-0.5.1.tar.gz
| Download URL | timeout_dead-0.5.1.tar.gz |
|---|---|
| Size | 247.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
32be5c62c3b2d1e4004dd0f9da531429f5db274edbb7d4e3ff7fd41631cdea05
|
|
BLAKE2b-256 checksum How to use checksums |
9a092aad8ab9b5bd577554cd215df58f326376fdc59ce3dbd0572647d6d86ae7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.
Transparency logRelease files / timeout_dead-0.5.1-py3-none-any.whl
| Download URL | timeout_dead-0.5.1-py3-none-any.whl |
|---|---|
| Size | 22.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7962a4f4a15c92c64e79dc60047a2907f852b21b1b97ddccddba155ae2732452
|
|
BLAKE2b-256 checksum How to use checksums |
871d59aaf5a2e569439de30ec786f5ea627be7f745b4ffb2a523d2c51928f496
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 16, 2026.
Transparency log