{"record":{"id":"a4e303b10b226f64","repo":"langchain-ai/deepagents","slug":"hook-process-stdin-is-unavailable","errorCode":null,"errorMessage":"Hook process stdin is unavailable","messagePattern":"Hook process stdin is unavailable","errorType":"exception","errorClass":"BrokenPipeError","httpStatus":null,"severity":"error","filePath":"libs/code/deepagents_code/hooks/runner.py","lineNumber":247,"sourceCode":"            await stdin_task\n    except BaseException:\n        stdin_task.cancel()\n        stdout_task.cancel()\n        stderr_task.cancel()\n        await asyncio.gather(\n            stdin_task,\n            stdout_task,\n            stderr_task,\n            return_exceptions=True,\n        )\n        raise\n    return stdout[0], stderr[0], stdout[1], stderr[1]\n\n\nasync def _write_input(process: Process, payload: bytes) -> None:\n    if process.stdin is None:\n        msg = \"Hook process stdin is unavailable\"\n        raise BrokenPipeError(msg)\n    process.stdin.write(payload)\n    await process.stdin.drain()\n    process.stdin.close()\n\n\nasync def _read_bounded(\n    stream: asyncio.StreamReader,\n    limit: int,\n) -> tuple[bytes, bool]:\n    retained = bytearray()\n    truncated = False\n    while chunk := await stream.read(_READ_CHUNK_BYTES):\n        remaining = limit - len(retained)\n        if remaining > 0:\n            retained.extend(chunk[:remaining])\n        if len(chunk) > remaining:\n            truncated = True\n    return bytes(retained), truncated","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/langchain-ai/deepagents/blob/a1af029e6e73cb17c36bff823d227747b28e91e1/libs/code/deepagents_code/hooks/runner.py#L229-L265","documentation":"`_write_input` sends the serialized hook payload to the subprocess stdin. If `process.stdin` is None, it raises `BrokenPipeError` because there is no writable pipe to deliver the payload through. This is a defensive check inside the writer task spawned by `_communicate_bounded`.","triggerScenarios":"The hook subprocess was created without `stdin=PIPE` or its stdin was already closed/consumed, so `process.stdin` is None when `_communicate_bounded` schedules `_write_input` to send the payload.","commonSituations":"Subprocess created with inherited stdin or devnull; reusing a Process whose stdin was already closed by a prior communicate call; custom runner wiring that closes stdin before writing.","solutions":["Spawn the hook process with `stdin=asyncio.subprocess.PIPE` so stdin is writable","Communicate with the process exactly once — do not close stdin or call another communicate before `_write_input` runs","Guard custom code paths: check `process.stdin is not None` before attempting to write"],"exampleFix":"// before\nproc = await asyncio.create_subprocess_exec(*argv)  # stdin inherited\n// after\nproc = await asyncio.create_subprocess_exec(*argv, stdin=asyncio.subprocess.PIPE, stdout=PIPE, stderr=PIPE)","handlingStrategy":"try-catch","validationCode":"if proc.stdin is None:\n    raise BrokenPipeError(\"hook subprocess was not created with stdin=PIPE\")","typeGuard":"def writable_stdin(p: asyncio.subprocess.Process) -> bool:\n    return p.stdin is not None and not p.stdin.is_closing()","tryCatchPattern":"try:\n    await _communicate_bounded(proc, payload, limit)\nexcept BrokenPipeError as e:\n    logger.error(\"cannot write hook payload: %s\", e)\n    proc.kill()","preventionTips":["Create the process with stdin=asyncio.subprocess.PIPE","Never close stdin or call communicate twice before the payload is written","Assert pipe availability before spawning writer tasks"],"tags":["brokenpipe","asyncio","subprocess","stdin","hooks"],"backgroundTag":"subprocess-pipe-unavailable","analyzedSha":"a1af029e6e73cb17c36bff823d227747b28e91e1","analyzedAt":"2026-08-29T11:43:24.718Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}