Merge pull request #1738 from entireio/fix/1398-hook-stdin-hang-windows · Entire

Merge pull request #1738 from entireio/fix/1398-hook-stdin-hang-windows

aacb4d6→main

Fix agent hooks hanging on stdin EOF on Windows/Git Bash (#1398)

Changes

168 unmodified lines

// Stream one JSON value rather than io.ReadAll so the hook never blocks
// waiting for stdin EOF that some agents don't send on Windows (issue #1398).
func (c *CopilotCLIAgent) readHookEnvelope(stdin io.Reader) (*hookEnvelope, error) {

data, err := io.ReadAll(stdin)

if err != nil {
    return nil, fmt.Errorf("failed to read hook input: %w", err)
}

return parseHookEnvelope(data)
}
func ReadAndParseHookInput[T any](stdin io.Reader) (*T, error) {

data, err := io.ReadAll(stdin)

if err != nil {
    return nil, fmt.Errorf("failed to read hook input: %w", err)
}

if len(data) == 0 {
    return nil, errors.New("empty hook input")
}

var result T
if err := json.Unmarshal(data, &result); err != nil {
    return nil, fmt.Errorf("failed to parse hook input: %w", err)
}
return &result, nil
}

Test cases

TestReadAndParseHookInput_ReturnsBeforeEOF

func TestReadAndParseHookInput_ReturnsBeforeEOF(t *testing.T) {
    t.Parallel()

pr, pw := io.Pipe()
    go func() {
        if _, err := pw.Write([]byte(`{"session_id":"s1","transcript_path":"/t.jsonl"}`)); err != nil {
            _ = pw.CloseWithError(err)
        }
    }()

// ... rest of the test code
}

TestReadHookInputRawLimited_ReturnsBeforeEOF

func TestReadHookInputRawLimited_ReturnsBeforeEOF(t *testing.T) {
    // ... setup and execution
}

TestReadAndParseHookInput_MalformedJSON

func TestReadAndParseHookInput_MalformedJSON(t *testing.T) {
    _, err := ReadAndParseHookInput[hookInput](strings.NewReader(`{"session_id": INVALID}`))
    if err == nil {
            ... handle error
    }
}

Additional Functions

func parseSubagentCheckpointHookInput(r io.Reader) (*SubagentCheckpointHookInput, error) {
    data, err := io.ReadAll(r)
    if err != nil {
        return nil, fmt.Errorf("failed to read input: %w", err)
    }

if len(data) == 0 {
        return nil, errors.New("empty input")
    }

var input SubagentCheckpointHookInput
    if err := json.Unmarshal(data, &input); err != nil {
        return nil, fmt.Errorf("failed to parse JSON: %w", err)
    }

return &input, nil
}