Exit Codes
shuck propagates exit codes from child processes with precision. Codes 0–123 are reserved for the child; 124–127 are reserved for shuck’s own errors (following GNU timeout conventions).
Exit Code Table
| Code | Source | Meaning |
|---|---|---|
| 0 | Child | Success |
| 1–123 | Child | Child-defined error (passthrough) |
| 124 | shuck | Child timed out |
| 125 | shuck | shuck internal error |
| 126 | shuck | Command found but not executable |
| 127 | shuck | Command not found |
Child Exit Codes (0–123)
shuck passes the child’s exit code through unchanged. If your command exits with code 1, shuck exits with code 1. If it exits with 42, shuck exits with 42.
shuck sh -c "exit 42"
echo $?
# 42
124 — Timeout
Emitted when the child process is still running when the timeout expires. Follows the GNU timeout convention.
shuck --timeout 1 sleep 10
echo $?
# 124
In JSON mode, timed_out will be true:
{
"stdout": "",
"exit_code": 124,
"timed_out": true,
...
}
125 — Internal Error
An error occurred in shuck itself, not in the child process. Examples:
- PTY allocation failed
- I/O error while reading from PTY
- JSON serialization failed
These indicate a bug or system-level issue. Please file an issue if you see this code.
126 — Not Executable
The command was found in $PATH but doesn’t have execute permissions.
chmod -x /tmp/my-script
shuck /tmp/my-script
echo $?
# 126
127 — Command Not Found
The command could not be found in $PATH or as an absolute path.
shuck nonexistent-command
echo $?
# 127
Agent Error Handling
Agents should handle exit codes programmatically to decide next steps:
Bash Pattern
#!/bin/bash
shuck --json my-interactive-tool -- "some input" > result.json
exit_code=$(jq .exit_code result.json)
case $exit_code in
0) echo "Success — process stdout" ;;
124) echo "Timed out — retry with longer timeout or abort" ;;
126) echo "Not executable — check permissions" ;;
127) echo "Not found — install the tool or check PATH" ;;
*) echo "Child failed with $exit_code — read stderr for details" ;;
esac
Python Pattern
import subprocess, json
result = subprocess.run(
["shuck", "--json", "psql", "-U", "postgres", "--", "\\dt"],
capture_output=True, text=True
)
data = json.loads(result.stdout)
if data["exit_code"] == 0:
# Success — parse the schema output
tables = data["stdout"]
elif data["exit_code"] == 124:
# Database query timed out
raise TimeoutError("psql timed out")
elif data["exit_code"] == 127:
# psql not installed
raise FileNotFoundError("psql not found in PATH")
else:
# Child process error
raise RuntimeError(f"psql failed ({data['exit_code']}): {data['stderr']}")
Decision Tree for Agents
- exit_code == 0: Command succeeded. Parse
stdoutfor results. - timed_out == true (exit_code 124): Command took too long. Retry with
--timeoutor report failure. - exit_code == 127: Tool not installed. Ask user to install it or use an alternative.
- exit_code == 126: Permission issue. Check file permissions.
- exit_code == 125: shuck bug. Report to maintainers.
- exit_code 1–123: Tool-specific error. Read
stderrfor details.
Checking Exit Codes in Scripts
#!/bin/bash
shuck my-interactive-tool -- "some input"
exit_code=$?
case $exit_code in
0) echo "Success" ;;
124) echo "Timed out" ;;
127) echo "Command not found" ;;
*) echo "Failed with exit code $exit_code" ;;
esac
JSON Mode Exit Codes
In --json mode, the exit code appears in both:
- The
exit_codefield in the JSON output - shuck’s own process exit code
result=$(shuck --json my-tool)
exit_code=$(echo "$result" | jq .exit_code)
POSIX Signal Mapping
On Unix, if the child is killed by a signal, the exit code is 128 + signal_number. shuck passes this through unchanged:
| Signal | Exit Code |
|---|---|
| SIGTERM (15) | 143 |
| SIGKILL (9) | 137 |
| SIGINT (2) | 130 |
Note: These are in the 128+ range which is above shuck’s reserved range (124–127), so there’s no conflict.