chenhg5/cc-connect · error

read hook input

Error message

read hook input: %w

What it means

Relay wraps the underlying io.ReadAll failure when reading the hook's stdin payload: 'read hook input: %w'. Agy pipes the hook invocation JSON into the hook's stdin; if that read fails at the OS level the error is wrapped and returned, causing the hook to fail closed (deny).

Solutions

  1. Verify the hook is invoked by agy normally (stdin piped) rather than run interactively
  2. Retry the triggering action in agy — usually transient
  3. Inspect the wrapped %w error for the exact OS cause (e.g. 'file already closed')
  4. If testing, pass a real JSON payload via stdin: `echo '{}' | hook`
Defensive patterns

Strategy: retry

Try / catch

if err := Relay(in, out, addr, tok); err != nil && strings.HasPrefix(err.Error(), "read hook input:") {
    log.Printf("transient stdin read failure, retrying: %v", err)
}

Prevention

When it happens

Trigger: io.ReadAll(io.LimitReader(in, maxHookInput+1)) returns err != nil — stdin closed prematurely, broken pipe from the parent agy process, or I/O error on the underlying fd.

Common situations: agy killed mid-hook invocation; hook stdin mis-wired when run manually for debugging; container/pty environments that close stdin early.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/7fb189147749c029. Report an issue: GitHub.

Appendix: source

Thrown at agent/antigravityhook/protocol.go:39

type BridgeRequest struct {
	Token     string          `json:"token"`
	HookInput json.RawMessage `json:"hook_input"`
}

type BridgeResponse struct {
	Decision string `json:"decision"`
	Reason   string `json:"reason,omitempty"`
}

// Relay forwards one Agy hook invocation to the owning cc-connect session.
func Relay(in io.Reader, out io.Writer, address, token string) error {
	if strings.TrimSpace(address) == "" || strings.TrimSpace(token) == "" {
		return fmt.Errorf("permission bridge environment is missing")
	}

	input, err := io.ReadAll(io.LimitReader(in, maxHookInput+1))
	if err != nil {
		return fmt.Errorf("read hook input: %w", err)
	}
	if len(input) > maxHookInput {
		return fmt.Errorf("hook input exceeds %d bytes", maxHookInput)
	}
	if !json.Valid(input) {
		return fmt.Errorf("hook input is not valid JSON")
	}

	conn, err := net.DialTimeout("tcp", address, bridgeDialTimeout)
	if err != nil {
		return fmt.Errorf("connect permission bridge: %w", err)
	}
	defer func() { _ = conn.Close() }()
	// The listener is started before agy runs this hook, so dial failures should
	// fail closed quickly. After connect, wait much longer for a human response.
	_ = conn.SetDeadline(time.Now().Add(bridgeResponseTimeout))

	if err := json.NewEncoder(conn).Encode(BridgeRequest{Token: token, HookInput: input}); err != nil {

View on GitHub (pinned to 4000b2338a)