claude-code icon indicating copy to clipboard operation
claude-code copied to clipboard

[BUG] Claude Code can't be spawned from node.js, but can be from python

Open Flux159 opened this issue 8 months ago • 10 comments

Environment

  • Platform (select one):
    • [x] Anthropic API
  • Claude CLI version: 0.2.69 (Claude Code)
  • Operating System: Mac OS 15.3.1, Node v23.10.0
  • Terminal: Terminal App

Bug Description

Claude code doesn't run inside of node script.

Steps to Reproduce

Try running this node script that shells out to claude code using -p with stream-json:

const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);

async function main() {
    try {
        const command = 'claude -p --dangerously-skip-permissions --output-format "stream-json" "ls -la"';
        console.log('Running command:', command);
        
        // Set a timeout to prevent hanging
        const timeout = 30000; // 30 seconds
        
        // Create a promise that rejects after the timeout
        const timeoutPromise = new Promise((_, reject) => {
            setTimeout(() => {
                reject(new Error(`Command timed out after ${timeout}ms`));
            }, timeout);
        });
        
        // Run the command with a timeout
        const result = await Promise.race([
            execAsync(command),
            timeoutPromise
        ]);
        
        console.log('Command completed successfully');
        console.log('stdout:', result.stdout);
        if (result.stderr) console.log('stderr:', result.stderr);
        
    } catch (error) {
        console.error('Error:', error.message);
    }
}

main(); 

Instead of returning streaming json outputs, it stalls.

Now try running with python:

import subprocess
import json
import sys
from datetime import datetime

def main():
    print(f"Starting Claude test at {datetime.now()}")
    
    # Command to run
    cmd = ['claude', '-p', '--dangerously-skip-permissions', '--output-format', 'stream-json', 'ls -la']
    
    try:
        # Run the command with a timeout
        print("Running command:", ' '.join(cmd))
        result = subprocess.run(
            cmd,
            capture_output=True,
            text=True,
            timeout=30  # 30 second timeout
        )
        
        print("\nExit code:", result.returncode)
        
        if result.stdout:
            print("\nStdout:")
            print(result.stdout)
            
            # Try to parse JSON output
            try:
                for line in result.stdout.splitlines():
                    if line.strip():
                        json_data = json.loads(line)
                        print("\nParsed JSON:")
                        print(json.dumps(json_data, indent=2))
            except json.JSONDecodeError as e:
                print("Failed to parse JSON:", e)
        
        if result.stderr:
            print("\nStderr:")
            print(result.stderr)
            
    except subprocess.TimeoutExpired:
        print("Command timed out after 30 seconds")
    except subprocess.CalledProcessError as e:
        print("Command failed with exit code:", e.returncode)
        print("Stderr:", e.stderr)
    except Exception as e:
        print("Error:", str(e))

if __name__ == "__main__":
    main() 

The python script can exec perfectly fine.

Expected Behavior

Node apps that use exec or spawn should be able to get results from claude code.

Actual Behavior

Node apps stall and don't return any results, yet python & shell works fine.

Additional Context

I tried with exec, spawn, using the same env vars, etc. and nothing worked in Node. Is claude code mutating a the node environment in some way such that subprocesses don't work correctly?

Flux159 avatar Apr 12 '25 04:04 Flux159

Got the same issue. The cli command run just fine, but using exec, spawn ... it just hang forever.

hop-tran-ftai avatar May 07 '25 13:05 hop-tran-ftai

Same issue here @Flux159 I have the problem in python in my case. Could it be because I don't specify --dangerously-skip-permissions ? I tried to add it and it return Stderr: --dangerously-skip-permissions must be accepted in an interactive session first.

I am not sure what should be done and how can I run it without any human interaction. Any ideas ?

yguezsealsec avatar May 11 '25 12:05 yguezsealsec

@yguezsealsec - Run Claude code one time w/ --dangerously-skip-permissions via terminal and accept it there, that will be cached for future runs then.

Alternatively you could try to remove that flag, but then I think you'll end up with issues anytime Claude code tries to do a tool call because it will stall waiting for input.

Flux159 avatar May 11 '25 15:05 Flux159

@Flux159 Thanks for you quick answer. I did that and run your script but I get timeout...do you know why ?

Also how can I run Claude code within Github Action ? My usecase is the following: I have a python lib where I should run claude code each time this lib in invoked. This lib will be invoked in another python script in github action. How can I run it ?

Thanks v.m for your precious help !

Image

yguezsealsec avatar May 11 '25 20:05 yguezsealsec

@yguezsealsec No don't know why it's timing out in python if you already accepted dangerous commands.

I don't think that this will work in CI, you probably want to make a separate issue for that since you won't be able to open the interactive accept in CI.

Flux159 avatar May 11 '25 21:05 Flux159

~FWIW, fork seems not to hang the same way spawn does.~ nvm, changed too many things at once.

It's "stdio": "inherit" that makes the difference ("stdio": "pipe" hangs)

This hangs:

spawn("claude", ["--print", `"Hello, world!"`]);

This doesn't:

spawn("claude", ["--print", `"Hello, world!"`], {
  stdio: ["inherit", "pipe", "pipe"],
});

In my use-case, I want to pipe the stdout + stderr so I can stream the output into memory. IIUC "pipe" is essentially the default if unspecified (source)

lukebjerring avatar May 24 '25 18:05 lukebjerring

Would be good if this bug were fixed. It's really frustrating that the error given is useless too.

jezweb avatar Jun 26 '25 03:06 jezweb

Image

AlanGreyjoy avatar Jun 27 '25 02:06 AlanGreyjoy

Working on a sub-agent design that spawns claude subagents as tools via mcp with specific contexts, aka "testing agent", "types agent".

Cannot for the life of me get claude to spawn with any above technique, tried ['inherit', 'pipe', 'pipe'] as @lukebjerring suggested, as well as other permutations.

hangs on the spawn call


    const claudeProcess = spawn('claude', ['mcp', 'serve'], {
      cwd: projectRoot,
      stdio: ['inherit', 'pipe', 'pipe'],
      env: {
        ...process.env,
        // implementation specific logic
        CLAUDE_AGENT_TYPE: agentType,
        CLAUDE_AGENT_NAME: agentName,
      },
    });

    claudeProcess.on('error', (error) => {
      console.error(`[${agentName}] Claude process error:`, error);
    });

    claudeProcess.on('exit', (code, signal) => {
      console.error(`[${agentName}] Claude process exited with code ${code}, signal ${signal}`);
    });

    claudeProcess.stderr.on('data', (data) => {
      console.error(`[${agentName}] Claude stderr:`, data.toString());
    });

EDIT: was able to get around this using

import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
const transport = new StdioClientTransport({
  command: 'claude',
  args: ['mcp', 'serve'],
  env: {
    ...process.env
  },
  cwd: projectRoot,
});

jlukic avatar Jun 27 '25 17:06 jlukic

There is a work around, but it's not easy to maintain. Create an .sh script and run that as a claude-code-wrapper.sh or something like that. Albeit, I don't use windows anymore and only use Linux Mint, so my dev experience is a lot more friendly than if you're on mac or windows and using WSL. I am currently in the process of getting a production ready solution using the .sh wrapper for docuforge.io for automated codebase documentation.

Spawn the wrapper with spawn and use promisify. Also make sure you log claude-code to file using promises from 'fs' so you can debug easier.

This is the best we have, until they figure out why it hangs in exec or spawn.

AlanGreyjoy avatar Jun 27 '25 18:06 AlanGreyjoy

I figured this out after many hours of hacking.

The problem is that spawn et al. pull in the environment; claude code needs the environment set to a very specific and undocumented setting.

Here is the solution:

      const child = spawn('claude', ['-p', command], {
        stdio: ['inherit', 'pipe', 'pipe'],
        env: {
          ...process.env,
          ANTHROPIC_API_KEY: '',
        }
      });

You can also echo and pipe your input using other common approaches as widely demonstrated on the internet.

Flamenco avatar Jul 07 '25 15:07 Flamenco

Credit to @Flux159 for also posting the Python example, because that was key in figuring this out!

Flamenco avatar Jul 07 '25 15:07 Flamenco

You can see an example of this in use in our github action code: https://github.com/anthropics/claude-code-action/blob/main/base-action/src/run-claude.ts#L171. You can also use the TypeScript SDK as an alternative to spawning Claude directly: https://docs.anthropic.com/en/docs/claude-code/sdk#type-script.

ashwin-ant avatar Aug 22 '25 19:08 ashwin-ant

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.

github-actions[bot] avatar Aug 31 '25 14:08 github-actions[bot]