Skip to content

Guide to Creating Python Tools

Jon edited this page Aug 30, 2026 · 2 revisions

Purpose

Ash can run your own Python scripts as AI tools, alongside allowlisted Unix commands. This page documents the interface a Python tool must implement, using the bundled yfinance.py and wikipedia.py scripts as the reference pattern, and how ash discovers, allowlists, and executes them.

Where scripts live

To make eligible managed scripts available automatically, retain the standalone $TOOLS_DIR_LIST directive in ~/.ash/.ash_tools. This is an internal Ash directive, not an environment variable. In .ash_system, $TOOLS_DIR_LIST and $IF_PYTHON_AVAILABLE are also Ash-internal substitutions and cannot be supplied through the environment. Replace the directive with literal bare script names for a fixed restrictive policy.

Required interface for a Python tool

Every bundled tool follows the same shape. Study yfinance.py or wikipedia.py end to end, but the required pieces are:

  1. Shebang and permissions — the first line must be #!/usr/bin/env python3, and the file must be a regular file with both read and execute permission bits (for example, chmod 700 tool.py), since ash runs it directly by bare name with no shell.
  2. argparse-based CLI — expose predictable, named options (--query, TICKER, etc.) rather than positional-only or ad hoc parsing, so the AI can construct calls reliably. See main() in both reference scripts.
  3. An --ai-docs flag — when passed, the script must print documentation and exit before validating any other required arguments. This is how the AI learns what the tool does, without needing separate, hand-maintained documentation. Compare build_ai_docs() in yfinance.py and wikipedia.py. The docs text should cover, at minimum:
    • Capabilities — what the tool does, in a sentence or two.
    • Arguments — every flag/positional, its meaning, and its default.
    • Return format — the JSON shape and what each field means.
    • Usage guidance — how the AI should interpret and present the result, including error handling.
  4. Structured JSON output on stdout — always emit a single JSON object (or object per item), never prose mixed with data. Include a status field (for example success, error, not_found) so the AI can branch reliably; put failure detail in a message/error field rather than letting the script crash with a traceback. Both reference scripts return status on every code path, including exceptions (see the try/except blocks in fetch_stock_data and wikipedia_search_tool).
  5. Bounded, well-behaved execution — set network timeouts, handle expected failures (wikipedia.py's make_request_with_backoff retries on HTTP 429 with backoff), and keep output bounded — ash also enforces a hard cap independently (see Execution limits below).

A minimal skeleton:

#!/usr/bin/env python3
import argparse
import json


def build_ai_docs() -> str:
    return """Capabilities:
- ...

Arguments:
- --thing TEXT: ...
- --ai-docs: Print this documentation and exit.

Return format:
- JSON object with keys: status, ...

Usage guidance for the AI:
- ...
"""


def main():
    parser = argparse.ArgumentParser(description="...")
    parser.add_argument("--thing", help="...")
    parser.add_argument("--ai-docs", action="store_true", help="Print AI usage documentation and exit.")
    args = parser.parse_args()

    if args.ai_docs:
        print(build_ai_docs())
        return

    try:
        result = {"status": "success", "...": "..."}
    except Exception as e:
        result = {"status": "error", "message": str(e)}

    print(json.dumps(result, indent=2))


if __name__ == "__main__":
    main()

Test coverage for the --ai-docs contract

test_tool_docs.py runs each bundled tool with --ai-docs and asserts the output contains Capabilities, Return format, and the tool's key argument names. Add a similar case for your own tool if it lives in this repository, or apply the same check manually for tools you keep only in ~/.ash/tools/.

Allowlisting your tool

Ash only executes allowlisted executables via run_unix_command/run_unix_pipeline. Add your script's bare filename (for example my_tool.py) to ~/.ash/.ash_tools, one per line or comma-separated, or set ASH_TOOL_ALLOWLIST for a one-off override. See the allowlist documentation in README.md.

The ash lifecycle for a Python tool

  1. Discovery via the system prompt — ~/.ash/.ash_system (installed from ash_bootstrap/.ash_system) tells the AI that scripts under ~/.ash/tools/ are allowlisted and executable directly by bare name, and instructs it to run <script> --ai-docs first to learn capabilities and arguments before using a tool. There is no separate Go-side tool registry for Python scripts — the AI itself discovers the interface at conversation time by invoking --ai-docs.
  2. Invocation — the AI calls the run_unix_command tool (defined in tools.go) with the script's bare name and argv, exactly like any other allowlisted Unix executable.
  3. Allowlist and argument checks — localToolShim.callRunUnixCommand in tools.go confirms the executable is on the allowlist and rejects any argument containing shell control characters (isBlockedArgument), since ash never invokes a shell.
  4. Interpreter resolution — for a bundled .py script, managedPythonScript in python_env.go resolves the bare filename to its installed path under ~/.ash/tools/, and the command is run with ashPythonInterpreter() — the ash-managed virtualenv interpreter at ~/.ash/venv when it's ready, falling back to system python3 otherwise. See Python-Environment for how that virtualenv is provisioned and why it exists. Your own scripts in ~/.ash/tools/ that need third-party packages should either avoid them or have their dependencies available to whichever interpreter ash resolves for them.
  5. Execution limits — every tool call runs through toolCommandRunner with a bounded timeout (ASH_TOOL_TIMEOUT, default 15s) and a bounded captured output size (ASH_TOOL_OUTPUT_MAX, default 8192 bytes); output beyond that limit is truncated. Design your JSON output to stay well within this bound.
  6. Result returned to the AI — stdout/stderr and exit status are captured into a toolCommandResult and handed back to the AI as the tool response, for it to parse and present to the user.

Related reading

Clone this wiki locally