Skip to content

Run command

run_command

RunCommandTool — execute shell commands with timeout and output truncation.

Registered as run_command via the axm.tools entry point.

RunCommandTool

Execute arbitrary shell commands with timeout and output truncation.

Runs the given command via subprocess.run in the resolved working directory, with configurable timeout and output size caps. The command string is executed verbatim by the shell, so this tool can run any shell command the calling process is permitted to run.

.. warning:: The _BLOCKED_PATTERNS denylist is a best-effort guardrail against obvious footguns (e.g. rm -rf /), not a security sandbox. It matches substrings and is bypassable by design (encoding, indirection, equivalent commands). Do not rely on it for isolation or to contain untrusted input — treat any command passed here as executing with the full privileges of the host process.

Registered as run_command via axm.tools entry point.

Source code in packages/axm-edit/src/axm_edit/tools/run_command.py
Python
class RunCommandTool:
    """Execute **arbitrary shell commands** with timeout and output truncation.

    Runs the given command via ``subprocess.run`` in the resolved working
    directory, with configurable timeout and output size caps. The command
    string is executed verbatim by the shell, so this tool can run *any*
    shell command the calling process is permitted to run.

    .. warning::
        The ``_BLOCKED_PATTERNS`` denylist is a **best-effort guardrail**
        against obvious footguns (e.g. ``rm -rf /``), **not** a security
        sandbox. It matches substrings and is **bypassable by design**
        (encoding, indirection, equivalent commands). Do not rely on it for
        isolation or to contain untrusted input — treat any command passed
        here as executing with the full privileges of the host process.

    Registered as ``run_command`` via axm.tools entry point.
    """

    agent_hint: str = (
        "Runs ARBITRARY shell commands in cwd with host privileges."
        " The blocked-command denylist is a best-effort guardrail against"
        " obvious footguns, NOT a security sandbox, and is bypassable by"
        " design. No cd/pipes/&&. Returns stdout+stderr truncated to 4K"
        " chars. Use for tests, builds, scripts."
    )

    @property
    def name(self) -> str:
        """Tool name used for MCP registration."""
        return "run_command"

    def execute(
        self,
        *,
        command: str | None = None,
        path: str = ".",
        cwd: str | None = None,
        timeout: int = _DEFAULT_TIMEOUT,
        **kwargs: object,
    ) -> ToolResult:
        """Execute a shell command.

        Args:
            command: Shell command string (required).
            path: Project root directory (default ".").
            cwd: Working directory, relative to root (optional).
            timeout: Timeout in seconds (default 30).

        Returns:
            ToolResult with stdout, stderr, exit_code, and timed_out.
        """
        root_str = path
        cwd_rel = cwd

        # ── Validate inputs ──────────────────────────────────────────
        if not command or not command.strip():
            return ToolResult(
                success=False,
                error="Missing required argument: command",
            )

        if _is_blocked(command):
            return ToolResult(
                success=False,
                error="Blocked command: this command is not allowed",
            )

        root = Path(root_str).resolve()
        if not root.is_dir():
            return ToolResult(
                success=False,
                error=f"Root is not a directory: {root_str}",
            )

        # Resolve cwd within the sandbox
        if cwd_rel:
            resolved_cwd = resolve_safe(root, cwd_rel)
            if resolved_cwd is None:
                return ToolResult(
                    success=False,
                    error=f"cwd escapes project root: {cwd_rel}",
                )
            if not resolved_cwd.is_dir():
                return ToolResult(
                    success=False,
                    error=f"cwd is not a directory: {cwd_rel}",
                )
            work_dir = resolved_cwd
        else:
            work_dir = root

        # ── Execute ──────────────────────────────────────────────────
        return _run(command, work_dir, timeout)
name property

Tool name used for MCP registration.

execute(*, command=None, path='.', cwd=None, timeout=_DEFAULT_TIMEOUT, **kwargs)

Execute a shell command.

Parameters:

Name Type Description Default
command str | None

Shell command string (required).

None
path str

Project root directory (default ".").

'.'
cwd str | None

Working directory, relative to root (optional).

None
timeout int

Timeout in seconds (default 30).

_DEFAULT_TIMEOUT

Returns:

Type Description
ToolResult

ToolResult with stdout, stderr, exit_code, and timed_out.

Source code in packages/axm-edit/src/axm_edit/tools/run_command.py
Python
def execute(
    self,
    *,
    command: str | None = None,
    path: str = ".",
    cwd: str | None = None,
    timeout: int = _DEFAULT_TIMEOUT,
    **kwargs: object,
) -> ToolResult:
    """Execute a shell command.

    Args:
        command: Shell command string (required).
        path: Project root directory (default ".").
        cwd: Working directory, relative to root (optional).
        timeout: Timeout in seconds (default 30).

    Returns:
        ToolResult with stdout, stderr, exit_code, and timed_out.
    """
    root_str = path
    cwd_rel = cwd

    # ── Validate inputs ──────────────────────────────────────────
    if not command or not command.strip():
        return ToolResult(
            success=False,
            error="Missing required argument: command",
        )

    if _is_blocked(command):
        return ToolResult(
            success=False,
            error="Blocked command: this command is not allowed",
        )

    root = Path(root_str).resolve()
    if not root.is_dir():
        return ToolResult(
            success=False,
            error=f"Root is not a directory: {root_str}",
        )

    # Resolve cwd within the sandbox
    if cwd_rel:
        resolved_cwd = resolve_safe(root, cwd_rel)
        if resolved_cwd is None:
            return ToolResult(
                success=False,
                error=f"cwd escapes project root: {cwd_rel}",
            )
        if not resolved_cwd.is_dir():
            return ToolResult(
                success=False,
                error=f"cwd is not a directory: {cwd_rel}",
            )
        work_dir = resolved_cwd
    else:
        work_dir = root

    # ── Execute ──────────────────────────────────────────────────
    return _run(command, work_dir, timeout)

render_text(*, stdout, stderr, exit_code, timed_out, truncated)

Render a compact LLM-facing view of a command result.

The header carries the exit code (with a marker when non-zero so a failure is impossible to miss) plus explicit timeout / truncated flags. stdout and stderr are embedded raw (not JSON-escaped), each under its own labelled section, so no output is ever lost relative to data — only the structural JSON noise is dropped.

Source code in packages/axm-edit/src/axm_edit/tools/run_command.py
Python
def render_text(
    *,
    stdout: str,
    stderr: str,
    exit_code: int,
    timed_out: bool,
    truncated: bool,
) -> str:
    """Render a compact LLM-facing view of a command result.

    The header carries the exit code (with a ``✗`` marker when non-zero so a
    failure is impossible to miss) plus explicit ``timeout`` / ``truncated``
    flags. ``stdout`` and ``stderr`` are embedded **raw** (not JSON-escaped),
    each under its own labelled section, so no output is ever lost relative to
    ``data`` — only the structural JSON noise is dropped.
    """
    flags = [f"exit {exit_code}"]
    if exit_code != 0 or timed_out:
        flags[0] += " ✗"
    if timed_out:
        flags.append("timeout")
    if truncated:
        flags.append("truncated")
    header = "run_command | " + " · ".join(flags)

    sections = [header]
    if stdout:
        sections.append("stdout:\n" + stdout.rstrip("\n"))
    if stderr:
        sections.append("stderr:\n" + stderr.rstrip("\n"))
    if not stdout and not stderr:
        sections.append("(no output)")
    return "\n".join(sections)