Skip to content

Windows: all hook events silently fail because the consolidated python3 -c block exceeds the ~32KB CreateProcess arglist limit #488

@xannasavin

Description

@xannasavin

Summary

On Windows (git-bash / msys2), every hook event silently fails because peon.sh passes ~30 KB of Python source to python3 -c "...". Windows' CreateProcess caps the full command line at ~32,767 characters, so the exec is rejected before Python ever runs. The failure is invisible because stderr on that line is redirected to /dev/nullpeon debug status keeps reporting log files: 0, and no sounds play on Stop / Notification / PermissionRequest / etc.

Manual sub-commands (peon preview, packs list, volume) work fine because they take shorter code paths that don't go through the consolidated block.

Environment

  • peon-ping: 2.17.3
  • OS: Windows 11 Pro (build 26200)
  • Shell: Git Bash / MSYS2
  • Python: 3.12.x
  • Installed via install.sh | bash to ~/.claude/hooks/peon-ping

Reproduction

With debug: true in config.json:

echo '{"hook_event_name":"Stop","session_id":"test","cwd":"/f/Git-Projects"}' \
  | bash ~/.claude/hooks/peon-ping/peon.sh

Expected: an entry in logs/peon-ping-<date>.log, a sound played via the msys2 backend.
Observed: silent exit, no log entries, no sound.

Temporarily removing the 2>/dev/null from line 4280 reveals the underlying error:

peon.sh: line 4280: .../python3: Argument list too long

peon debug status still shows log files: 0 no matter how many hook events fire.

Root cause

peon.sh lines ~3341–4280:

_PEON_PYOUT=$(python3 -c "
  <~30 KB of inline Python, with bash-interpolated $vars
   and the injected ${_PEON_STATE_PY_HELPERS} block>
" <<< "$INPUT" 2>/dev/null)

python3 -c "<code>" passes <code> as argv[2], which goes through Windows' CreateProcess command-line length limit. The consolidated block (added for the "~120–200 ms faster hook response" optimization) plus the interpolated state-helpers easily exceeds 32 KB.

Linux execve and macOS limits are ~2 MB, so this never surfaces there.

Local workaround (confirmed fix)

Write the Python to a temp file and invoke it by path; $INPUT still arrives via stdin:

_PEON_PY_TMP=$(mktemp -t peon-ping-XXXXXX.py 2>/dev/null || echo "/tmp/peon-ping-$$.py")
cat > "$_PEON_PY_TMP" <<__PEON_PY_EOF__
  <same Python block — unquoted heredoc preserves the same $/`/\ expansion
   that the current double-quoted `python3 -c "..."` already does,
   so the bash-interpolated variables still work unchanged>
__PEON_PY_EOF__
_PEON_PYOUT=$(python3 "$_PEON_PY_TMP" <<< "$INPUT" 2>/dev/null)
rm -f "$_PEON_PY_TMP"

After applying locally against v2.17.3, Stop / Notification / PermissionRequest events start logging and playing sounds immediately. Tested on Windows 11 + git-bash + Python 3.12.

Suggested upstream fix

Two reasonable approaches:

  1. Temp file — what the workaround above uses. Small diff, preserves the existing bash-interpolation behavior the code already relies on, no Python rewrite needed.
  2. python3 - — needs redesigning how $INPUT gets delivered (currently on stdin), so less clean.

Option 1 is the smaller change and matches the existing architecture. Happy to send a PR if helpful.

Secondary suggestion

Line 4280 unconditionally swallows stderr (2>/dev/null). Even a one-line message when debug: true or $PEON_DEBUG=1 would have made this obvious instead of an untraceable silent failure. Something like:

if [ "${PEON_DEBUG:-0}" = "1" ] || [ "$(jq -r .debug "$CONFIG")" = "true" ]; then
  _PEON_PYOUT=$(python3 ... <<< "$INPUT")
else
  _PEON_PYOUT=$(python3 ... <<< "$INPUT" 2>/dev/null)
fi

Worth considering as a general observability improvement — right now, any Python-side crash in that block on any platform will look identical to this Windows issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions