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

CodeSourceMeaning
0ChildSuccess
1–123ChildChild-defined error (passthrough)
124shuckChild timed out
125shuckshuck internal error
126shuckCommand found but not executable
127shuckCommand 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

  1. exit_code == 0: Command succeeded. Parse stdout for results.
  2. timed_out == true (exit_code 124): Command took too long. Retry with --timeout or report failure.
  3. exit_code == 127: Tool not installed. Ask user to install it or use an alternative.
  4. exit_code == 126: Permission issue. Check file permissions.
  5. exit_code == 125: shuck bug. Report to maintainers.
  6. exit_code 1–123: Tool-specific error. Read stderr for 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:

  1. The exit_code field in the JSON output
  2. 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:

SignalExit 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.