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

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 — the first line must be #!/usr/bin/env python3, and the file must be executable (chmod +x), 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