shareAI-lab/learn-claude-code · error · ValueError

Only Bash commands can run in the background

Error message

Only Bash commands can run in the background

What it means

Raised by BackgroundManager.start() in s11_background_tasks/code.py:316 when asked to run a tool block whose name is not 'bash'. Background execution in s11 is implemented by shelling out the command in a worker thread, so only Bash command blocks — which carry a 'command' string to execute — are supported. The check is an exact string comparison on block.name before any input validation.

Source

Thrown at s11_background_tasks/code.py:316

        output = handler(**block.input) if handler else f"Unknown: {block.name}"
    except Exception as error:
        output = f"Error: {error}"
    return str(output)


# -- New in s11: background execution --

class BackgroundManager:
    def __init__(self):
        self.tasks: dict[str, dict] = {}
        self.results: dict[str, str] = {}
        self._ready: list[str] = []
        self._counter = 0
        self._lock = threading.Lock()

    def start(self, block) -> str:
        if block.name != "bash":
            raise ValueError("Only Bash commands can run in the background")
        command = block.input.get("command")
        if not isinstance(command, str) or not command.strip():
            raise ValueError("Bash command cannot be empty")

        with self._lock:
            self._counter += 1
            task_id = f"bg_{self._counter:04d}"
            self.tasks[task_id] = {
                "tool_use_id": block.id,
                "command": command,
                "status": "running",
            }

        thread = threading.Thread(
            target=self._run,
            args=(task_id, command),
            daemon=True,
        )

View on GitHub (pinned to 985456f4ad)

Solutions

  1. Only call start() with blocks whose name is exactly 'bash'; gate the call with `if block.name == 'bash':`.
  2. For non-bash tools, execute them synchronously through their normal handler instead of the background manager.
  3. If you added a new shell-like tool, extend the check to accept it explicitly rather than bypassing it.

Example fix

# before
bg_id = background.start(block)  # any tool block

# after
if block.name == 'bash':
    bg_id = background.start(block)
else:
    result = run_tool_sync(block)
Defensive patterns

Strategy: type-guard

Validate before calling

def block_is_backgroundable(block) -> bool:
    return getattr(block, 'name', None) == 'bash'

Type guard

def is_bash_block(block) -> bool:
    return getattr(block, 'name', '') == 'bash'

Try / catch

try:
    bg_id = background.start(block)
except ValueError as e:
    if 'Only Bash' in str(e):
        result = run_tool_sync(block)  # fall back to synchronous execution
    else:
        raise

Prevention

When it happens

Trigger: Passing a tool-call block whose .name is 'read', 'edit', 'write', 'grep', or any non-'bash' tool to BackgroundManager.start(); routing agent tool_use blocks to the background manager without filtering by name; constructing a synthetic block dict/object with the wrong name casing ('Bash').

Common situations: A dispatcher that forwards all tool calls to the background manager for uniformity; agent code that decides to 'run this in the background' for an edit or read operation; refactors that rename the bash tool constant without updating this comparison.

Related errors


AI-assisted analysis of shareAI-lab/learn-claude-code@985456f4ad (2026-08-14). Data as JSON: /api/errors/08ea1cc8f4d5a342. Report an issue: GitHub.