Skip to content

fix(worker): wrap Claude CLI in PTY to fix stdout buffering hang - #1678

Merged
serrrfirat merged 1 commit into
nearai:stagingfrom
j-bloggs:fix/claude-bridge-pty-buffering
Mar 28, 2026
Merged

serrrfirat merged 1 commit into
nearai:stagingfrom
j-bloggs:fix/claude-bridge-pty-buffering

Conversation

@j-bloggs

@j-bloggs j-bloggs commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Replace script -qfc shell-string approach with pty-process crate for PTY allocation
  • Revert to Command::arg() chaining (injection-safe by construction via execve)
  • Eliminates all shell injection surfaces: prompt, model, session_id
  • Keep stderr on separate pipe to prevent NDJSON parse breakage
  • Gate PTY behind #[cfg(unix)] with direct-spawn fallback for Windows CI
  • Remove shell_escape() function and tests (no longer needed)

Change Type

  • Bug fix (non-breaking change which fixes an issue)

Linked Issue

Addresses review feedback from zmanian and gemini-code-assist on this PR.

Validation

  • cargo fmt --all -- --check: pass
  • cargo clippy (all-features, default, libsql): pass (zero warnings)
  • cargo test --lib -- claude_bridge: 23 tests pass
  • pty-process crate: MIT licensed, depends on nix (MIT)

Security Impact

  • Eliminates entire class of shell injection vulnerabilities by removing the shell interpretation layer
  • Previously: prompt, model name, and session_id were interpolated into a shell string for script -qfc
  • Now: all arguments passed via execve (Command::arg), never shell-interpreted

Database Impact

None

Blast Radius

  • New dependency: pty-process 0.5 (MIT, Unix only via cfg gate)
  • Claude bridge stdout now read from PTY master instead of child.stdout
  • Windows builds use non-PTY fallback (compile-only, bridge runs in Linux Docker)

Rollback Plan

Revert to staging Command::new("claude").arg() without PTY (re-introduces buffering hang but eliminates security risk).

Review Track

Track C - runtime changes in src/worker/, new dependency

Feature Parity

No FEATURE_PARITY.md changes needed.

@github-actions github-actions Bot added size: M 50-199 changed lines scope: worker Container worker risk: medium Business logic, config, or moderate-risk modules labels Mar 26, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical bug where the Claude bridge would hang due to Node.js full stdout buffering when interacting with the Claude CLI via non-TTY pipes. By introducing a pseudo-terminal wrapper, the output is now line-buffered, ensuring proper streaming. Concurrently, this change enhances security by implementing necessary shell escaping for user-provided inputs, mitigating potential command injection risks within the worker.

Highlights

  • PTY Allocation: The Claude CLI invocation is now wrapped in script -qfc to allocate a pseudo-terminal (PTY).
  • Stdout Buffering Fix: This change resolves an issue where Node.js full-buffered stdout on non-TTY pipes, which previously caused the Claude bridge to hang indefinitely.
  • Shell Escaping: Robust shell escaping has been implemented for the prompt and session ID to prevent command injection vulnerabilities.
  • Regression Test: A new test case (prompt_shell_escaping) was added to validate the correctness and security of the shell escaping mechanism.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@github-actions github-actions Bot added the contributor: new First-time contributor label Mar 26, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the execution of the claude command by wrapping it in script -qfc to allocate a PTY, addressing Node.js stdout buffering issues. It also introduces shell escaping for the prompt argument to prevent command injection, and a new test prompt_shell_escaping to verify this. The review identifies a potential command injection vulnerability with resume_session_id due to a lack of proper escaping and suggests adding test cases for resume_session_id escaping to improve robustness.

Comment thread src/worker/claude_bridge.rs Outdated
Comment thread src/worker/claude_bridge.rs Outdated
@j-bloggs
j-bloggs force-pushed the fix/claude-bridge-pty-buffering branch from 4a30bf2 to ad67857 Compare March 26, 2026 11:53

@zmanian zmanian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: fix(worker): wrap Claude CLI in PTY to fix stdout buffering hang

PTY is the right fix for Node.js stdout buffering, but the implementation introduces security risk.

Critical

  1. Shell injection via unescaped self.config.model: Interpolated directly into the shell command string. Must be escaped with shell_escape() like prompt and session_id are.

High

  1. Security regression from Command::arg() to shell string: Previous code passed args via execve (injection-safe by construction). New code goes through script -qfc which invokes /bin/sh -c. Consider using the pty-process crate for PTY allocation while keeping Command::arg().

Medium

  1. script -qfc is GNU-only: macOS script has different syntax. Documented as Docker-only but a maintenance trap for local testing.

  2. stdout/stderr separation through PTY: script can merge streams. Verify NDJSON parsing still works when stderr lines from Claude CLI are potentially interleaved into stdout.

Positive

  • Thorough escaping tests covering quotes, metacharacters, adversarial session IDs.

Fix the model escaping at minimum. Strongly consider the PTY crate approach to avoid the shell interpretation layer entirely.

@j-bloggs
j-bloggs force-pushed the fix/claude-bridge-pty-buffering branch from ad67857 to befb6f3 Compare March 27, 2026 11:26
@github-actions github-actions Bot added scope: dependencies Dependency updates size: L 200-499 changed lines and removed size: M 50-199 changed lines labels Mar 27, 2026
- Add pty-process crate (MIT, tokio async support) for PTY allocation
- Spawn claude CLI with pty-process::Command::arg() chaining instead of
  building a shell string for script -qfc
- Eliminates all shell injection surfaces: prompt, model, session_id
  are passed via execve, never interpreted by a shell
- Keep stderr on separate pipe to prevent NDJSON parse breakage
  (pty-process attaches PTY to all fds by default)
- Gate PTY behind #[cfg(unix)] with direct-spawn fallback for Windows CI
- Read stdout from PTY master (implements tokio::io::AsyncRead)
- Add regression tests: arg vector construction + PTY allocation

Addresses review feedback from zmanian and gemini-code-assist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@j-bloggs
j-bloggs force-pushed the fix/claude-bridge-pty-buffering branch from befb6f3 to a3de012 Compare March 27, 2026 12:07

@zmanian zmanian left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review: shell injection via script -qfc is fully resolved -- replaced with pty-process crate using execve directly. No shell involved. Arguments passed via Command::arg(). Stderr kept separate from PTY. Excellent fix. Approve.

@serrrfirat
serrrfirat merged commit de5a1c7 into nearai:staging Mar 28, 2026
14 checks passed
DougAnderson444 pushed a commit to DougAnderson444/ironclaw that referenced this pull request Mar 29, 2026
…PTY (nearai#1678)

- Add pty-process crate (MIT, tokio async support) for PTY allocation
- Spawn claude CLI with pty-process::Command::arg() chaining instead of
  building a shell string for script -qfc
- Eliminates all shell injection surfaces: prompt, model, session_id
  are passed via execve, never interpreted by a shell
- Keep stderr on separate pipe to prevent NDJSON parse breakage
  (pty-process attaches PTY to all fds by default)
- Gate PTY behind #[cfg(unix)] with direct-spawn fallback for Windows CI
- Read stdout from PTY master (implements tokio::io::AsyncRead)
- Add regression tests: arg vector construction + PTY allocation

Addresses review feedback from zmanian and gemini-code-assist.

Co-authored-by: j-bloggs <j-bloggs@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
drchirag1991 pushed a commit to drchirag1991/ironclaw that referenced this pull request Apr 8, 2026
…PTY (nearai#1678)

- Add pty-process crate (MIT, tokio async support) for PTY allocation
- Spawn claude CLI with pty-process::Command::arg() chaining instead of
  building a shell string for script -qfc
- Eliminates all shell injection surfaces: prompt, model, session_id
  are passed via execve, never interpreted by a shell
- Keep stderr on separate pipe to prevent NDJSON parse breakage
  (pty-process attaches PTY to all fds by default)
- Gate PTY behind #[cfg(unix)] with direct-spawn fallback for Windows CI
- Read stdout from PTY master (implements tokio::io::AsyncRead)
- Add regression tests: arg vector construction + PTY allocation

Addresses review feedback from zmanian and gemini-code-assist.

Co-authored-by: j-bloggs <j-bloggs@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: new First-time contributor risk: medium Business logic, config, or moderate-risk modules scope: dependencies Dependency updates scope: worker Container worker size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants