NAME
runprompt — Run LLM prompts from your shell
SYNOPSIS
pip install gitINFO
DESCRIPTION
Run LLM prompts from your shell
README
runprompt
Run LLM .prompt files from your shell with a single-file Python script.
Dotprompt is an prompt template format for LLMs where a .prompt file contains the prompt and metadata (model, schema, config) in a single file. You can use it to run LLM prompts and get structured responses right in your shell.
Quick start | Examples | Tools | Template syntax | Configuration | Providers | Caching | Spec compliance
Quick start
Download the single-file script:
curl -O https://raw.githubusercontent.com/chr15m/runprompt/main/runprompt
chmod +x runprompt
Or you can run it directly without installing using uvx:
uvx --from git+https://github.com/chr15m/runprompt runprompt hello.prompt
Or install it via pip or uv to use it as a library:
# Using uv (recommended) uv pip install git+https://github.com/chr15m/runpromptUsing pip
pip install "git+https://github.com/chr15m/runprompt.git"
Create hello.prompt:
---
model: anthropic/claude-sonnet-4-20250514
---
Say hello to {{name}}!
Run it:
export ANTHROPIC_API_KEY="your-key"
./runprompt hello.prompt '{"name": "World"}'
# alternatively pass data via STDIN.
echo '{"name": "World"}' | ./runprompt hello.prompt
(You can get an Anthropic key from here: https://console.anthropic.com/settings/keys see below for other keys.)
Examples
In addition to the following, see the tests folder for more example .prompt files.
Basic prompt with stdin
---
model: anthropic/claude-sonnet-4-20250514
---
Summarize this text: {{STDIN}}
cat article.txt | ./runprompt summarize.prompt
The special {{STDIN}} variable always contains the raw stdin as a string. Both {{STDIN}} and {{ARGS}} are always available; if input is JSON it's also parsed into individual variables.
Command line arguments
Pass arguments directly on the command line:
---
model: anthropic/claude-sonnet-4-20250514
---
Process this: {{ARGS}}
./runprompt process.prompt Hello world, please summarize this text.
The special {{ARGS}} variable contains all arguments after the prompt file, joined with spaces. The {{INPUT}} variable contains STDIN if provided, otherwise ARGS.
Structured JSON output
Extract structured data using an output schema:
---
model: anthropic/claude-sonnet-4-20250514
input:
schema:
text: string
output:
format: json
schema:
name?: string, the person's name
age?: number, the person's age
occupation?: string, the person's job
---
Extract info from: {{text}}
echo "John is a 30 year old teacher" | ./runprompt extract.prompt
# {"name": "John", "age": 30, "occupation": "teacher"}
Schema uses Picoschema format. Fields ending with ? are optional. The format is field: type, description.
Chaining prompts
Pipe structured output between prompts:
echo "John is 30" | ./runprompt extract.prompt | ./runprompt generate-bio.prompt
The JSON output from the first prompt becomes template variables in the second.
Pre-prompt shell commands
Execute shell commands before the prompt is sent to gather dynamic context:
---
model: anthropic/claude-sonnet-4-20250514
before:
latest_commit: git log -1 --oneline
current_date: date
file_count: find . -name "*.py" | wc -l
---
Latest commit: {{latest_commit}}
Current date: {{current_date}}
Python files: {{file_count}}
Each command runs in your configured shell ($SHELL, defaulting to /bin/sh) with full shell features (pipes, redirects, etc.). On success, stdout is captured. On failure, stderr is captured. All outputs are available as individual variables plus a combined {{BEFORE}} variable.
Template variables are passed as environment variables to the shell, so you can reference them:
---
model: anthropic/claude-sonnet-4-20250514
before:
model_info: echo "Using model: ${model}"
---
{{model_info}}
Executable prompt files
Make .prompt files directly executable with a shebang:
#!/usr/bin/env runprompt
---
model: anthropic/claude-sonnet-4-20250514
---
Hello, I'm {{name}}!
chmod +x hello.prompt
echo '{"name": "World"}' | ./hello.prompt
Note: runprompt must be in your PATH, or use a relative/absolute path in the shebang (e.g. #!/usr/bin/env ./runprompt).
CLI overrides
Override frontmatter values from the command line:
./runprompt --model anthropic/claude-haiku-4-20250514 hello.prompt
./runprompt --output.format json extract.prompt
Use dot notation for nested values (e.g. --output.format). Pass template variables via stdin or as JSON argument:
echo '{"name": "Alice"}' | ./runprompt hello.prompt
./runprompt hello.prompt '{"name": "Alice"}'
See --help for all options.
File attachments
Attach local files (supports globs) and URLs to the prompt context.
From frontmatter:
---
model: anthropic/claude-sonnet-4-20250514
files:
- README.md
- src/**/*.py
---
Review these files and suggest improvements.
You can use template variables (including before: outputs) in file patterns:
---
model: anthropic/claude-sonnet-4-20250514
before:
changed_files: git diff --name-only HEAD~1
files:
- "{{changed_files}}"
---
Review the recently changed files.
From the CLI (also supports globs). --file is an alias for --read:
./runprompt --read README.md --read "src/**/*.py" review.prompt
./runprompt --file README.md review.prompt
You can combine both: frontmatter files: entries plus any --read/--file
flags are all included.
When attachments are loaded, runprompt prints a yellow message to stderr for
each loaded file/URL.
Tools
Tools allow the LLM to call Python functions during prompt execution. Define tools as Python functions with docstrings, and the LLM can use them to perform actions like reading files, making API calls, or interacting with the system.
Defining tools
Create a Python file with functions. Any function with a docstring becomes a tool:
# my_tools.py def get_weather(city: str): """Gets the current weather for a city.Returns the temperature and conditions for the specified location. """ # Your implementation here return {"temp": 72, "conditions": "sunny"}def calculate(expression: str): """Evaluates a mathematical expression.
Use this for arithmetic calculations. """ return eval(expression)
Using tools in prompts
Reference tools in the frontmatter using Python import syntax:
---
model: anthropic/claude-sonnet-4-20250514
tools:
- my_tools.*
---
What's the weather in Tokyo and what's 15% of 847?
Tool specifications:
module.*- Import all functions with docstrings frommodule.pymodule.function_name- Import a specific function
Running prompts with tools
When the LLM wants to call a tool, you'll be prompted to confirm:
$ ./runprompt weather.promptI'll check the weather for you.
Tool call: get_weather Arguments: {"city": "Tokyo"}
Run this tool? [y/n]: y
The weather in Tokyo is currently 72°F and sunny.
Safe tools
Mark tools as "safe" to allow them to run without confirmation when using --safe-yes, env var RUNPROMPT_SAFE_YES, or config safe_yes:
# my_tools.py def get_weather(city: str): """Gets the current weather for a city (read-only operation).""" return {"temp": 72, "conditions": "sunny"}get_weather.safe = True # Mark as safe
def delete_file(path: str): """Deletes a file from the filesystem.""" os.remove(path) return "deleted"
Not marked as safe - will always prompt for confirmation
Run with --safe-yes or env var RUNPROMPT_SAFE_YES or config var safe_yes to auto-approve safe tools:
./runprompt --safe-yes weather.prompt
With --safe-yes:
- Tools marked with
fn.safe = Truerun without prompting - Tools without the safe attribute still prompt for confirmation
This is useful for automation where you trust certain read-only or low-risk operations but still want confirmation for potentially dangerous actions.
Tool import paths
Tools are searched for in:
- The current working directory
- The directory containing the prompt file
- Additional paths via
--tool-pathor config file - Default config tool directories (if they exist):
./.runprompt/tools$XDG_CONFIG_HOME/runprompt/tools(default:~/.config/runprompt/tools)~/.runprompt/tools
./runprompt --tool-path ./my_tools --tool-path /shared/tools prompt.prompt
All paths are searched in addition to any tool_path entries in config files.
Type hints
Type hints on function parameters map to JSON Schema types:
| Python | JSON Schema |
|---|---|
str | string |
int | integer |
float | number |
bool | boolean |
list | array |
dict | object |
Parameters without type hints default to string.
Error handling
If a tool raises an exception, the error is sent back to the LLM which can decide how to proceed:
Tool call: read_file Arguments: {"path": "missing.txt"}Run this tool? [y/n]: y FileNotFoundError: [Errno 2] No such file or directory: 'missing.txt'
I couldn't read that file because it doesn't exist. Would you like me to try a different path?
Shell tools
Define simple shell script tools inline in your prompt:
---
model: anthropic/claude-sonnet-4-20250514
shell_tools:
git_status: git status --short
count_py_files: find . -name "*.py" | wc -l
---
What's the current git status and how many Python files are there?
Long form with options:
shell_tools:
git_log:
cmd: git log --oneline
safe: true
description: Show recent git commits
search_code:
cmd: grep -r
safe: true
description: Search for text in files
Fields:
cmd(required): The shell command to executesafe(optional, default: false): Mark as safe for auto-approval with--safe-yesdescription(optional, default: cmd): Description shown to the LLM
Arguments:
The LLM can pass:
args(string): Appended to the command- Environment variables as named parameters
Example LLM call:
git_log(args="--author=alice -n 5")
search_code(args="TODO", PATH="/src")
Shell tools use the same shell resolution as before: commands ($SHELL, defaulting to /bin/sh).
Builtin tools
Runprompt includes builtin tools that can be used without creating external Python files:
---
model: anthropic/claude-sonnet-4-20250514
tools:
- builtin.fetch_clean
---
Please summarize this page: {{ARGS}}
Available builtin tools:
| Tool | Description |
|---|---|
calculator | Safely evaluate mathematical expressions (arithmetic, trig, log, etc.) |
fetch_clean | Fetch a URL and return a lightweight version of the page for LLM consumption |
datetime | Return current date and time in ISO format (YYYY-MM-DDTHH:MM:SS) |
sleep | Sleep for the specified number of seconds |
ask_user | Ask the user a question and return their response |
shell | Run a shell command and return stdout/stderr/returncode |
write_file('path') | Write content to a specific file (parameterized) |
The calculator, datetime, fetch_clean, sleep, and ask_user tools are marked as safe (auto-approved with --safe-yes). The shell and write_file tools require confirmation since they can run commands / modify the filesystem.
Parameterized tools
Some builtin tools are "factories" that take arguments to create a specialized tool:
---
model: anthropic/claude-sonnet-4-20250514
tools:
- builtin.write_file('output.txt')
---
Write a haiku about coding to the file.
The write_file('output.txt') creates a tool that can only write to
output.txt. The LLM provides the content, but cannot choose the path - this
is a security feature that limits what files the LLM can modify. The created
tool is exposed to the LLM with a filename-specific name (slug), e.g.
write_file_output_txt.
Use builtin.* to import all builtin tools, or builtin.tool_name for a specific one.
Underscore prefix
Files or functions starting with _ are excluded from wildcard imports:
# _helpers.py - this entire file is excluded from wildcard imports # my_tools.py def _private_helper(): # excluded from wildcard imports """Internal helper function.""" pass
def public_tool(): # included in wildcard imports """A tool available to the LLM.""" pass
Use _ prefix for helper functions you don't want exposed as tools.
Template syntax
Templates use a useful subset of Handlebars/Mustache syntax.
Special variables
{{STDIN}}- Raw stdin content (always available){{ARGS}}- Command line arguments after prompt file (always available){{INPUT}}- STDIN if provided, otherwise ARGS (always available){{BEFORE}}- Combined output from allbefore:commands- Individual variables from
before:commands (e.g.{{latest_commit}})
Supported features
- Variable interpolation:
{{variableName}},{{object.property}} - Comments:
{{! this is a comment }} - Conditionals:
{{#if key}}...{{/if}},{{#if key}}...{{else}}...{{/if}} - Negated conditionals:
{{#unless key}}...{{/unless}},{{#unless key}}...{{else}}...{{/unless}} - Iteration:
{{#each items}}...{{/each}}with@index,@first,@last,@key - Sections:
{{#key}}...{{/key}}(renders if truthy) - Inverted sections:
{{^key}}...{{/key}}(renders if falsy)
Falsy values: false, 0, "" (empty string), [] (empty list), missing/undefined variables.
Configuration
Configuration values can be set from config file, env var or command line flag, with flags overriding env vars which override config file settings.
- Config files (lowest priority, loaded in order):
~/.runprompt/config.yml$XDG_CONFIG_HOME/runprompt/config.yml(default:~/.config/runprompt/config.yml)./.runprompt/config.yml(project-local)
- Environment variables (
RUNPROMPT_*prefix) - CLI flags (highest priority)
Config options
| Option | Config file | Environment variable | CLI flag |
|---|---|---|---|
| Model | model: openai/gpt-4o | RUNPROMPT_MODEL | --model |
| Default model | default_model: openai/gpt-4o | RUNPROMPT_DEFAULT_MODEL | --default-model |
| Base URL | base_url: http://... | RUNPROMPT_BASE_URL | --base-url |
| Tool paths | tool_path: [./tools] | RUNPROMPT_TOOL_PATH | --tool-path |
| Cache | cache: true | RUNPROMPT_CACHE=1 | --cache |
| Cache dir | cache_dir: /path | RUNPROMPT_CACHE_DIR | --cache-dir |
| Safe yes | safe_yes: true | RUNPROMPT_SAFE_YES=1 | --safe-yes |
| Verbose | verbose: true | RUNPROMPT_VERBOSE=1 | --verbose |
API keys
API keys can be set via config file, RUNPROMPT_* env var, or native env var:
| Provider | Config file | RUNPROMPT env var | Native env var |
|---|---|---|---|
| Anthropic | anthropic_api_key: sk-... | RUNPROMPT_ANTHROPIC_API_KEY | ANTHROPIC_API_KEY |
| OpenAI | openai_api_key: sk-... | RUNPROMPT_OPENAI_API_KEY | OPENAI_API_KEY |
| Google AI | google_api_key: ... | RUNPROMPT_GOOGLE_API_KEY | GOOGLE_API_KEY |
| OpenRouter | openrouter_api_key: ... | RUNPROMPT_OPENROUTER_API_KEY | OPENROUTER_API_KEY |
Priority for API keys: config file, env var, then flag as fallback.
Config file example
# ./.runprompt/config.yml, ~/.config/runprompt/config.yml, or ~/.runprompt/config.yml
model: openai/gpt-4o
default_model: anthropic/claude-sonnet-4-20250514 # fallback if model not set anywhere
cache: true
safe_yes: true
tool_path:
- ./tools
- /shared/tools
openai_api_key: sk-...
The default_model is used as a fallback when no model is specified in the prompt file, config, environment, or CLI. This lets you set a preferred model that's used only when nothing else specifies one.
Custom endpoint (Ollama, etc.)
Use base_url to point at any OpenAI-compatible endpoint:
# Via config file # base_url: http://localhost:11434/v1Via environment variable
export RUNPROMPT_BASE_URL="http://localhost:11434/v1"
Via CLI flag
./runprompt --base-url http://localhost:11434/v1 hello.prompt
Legacy env vars also work (checked in this order)
export OPENAI_BASE_URL="http://localhost:11434/v1" export OPENAI_API_BASE="http://localhost:11434/v1" # OpenAI SDK v0.x style export BASE_URL="http://localhost:11434/v1"
When a custom base URL is set, the provider prefix in the model string is ignored and the OpenAI-compatible API format is used.
Verbose mode
Use -v or set verbose: true to see request/response details:
./runprompt -v hello.prompt
Providers
Models are specified as provider/model-name:
| Provider | Model format | API key |
|---|---|---|
| Anthropic | anthropic/claude-sonnet-4-20250514 | Get key |
| OpenAI | openai/gpt-4o | Get key |
| Google AI | googleai/gemini-1.5-pro | Get key |
| OpenRouter | openrouter/anthropic/claude-sonnet-4-20250514 | Get key |
OpenRouter provides access to models from many providers (Anthropic, Google, Meta, etc.) through a single API key.
Caching
Enable response caching to avoid redundant API calls during development:
# Enable caching with -c or --cache ./runprompt --cache hello.promptSecond run with same input uses cached response
./runprompt --cache hello.prompt
You can also enable the cache across a whole pipeline with the env var:
export RUNPROMPT_CACHE=1; echo "..." | ./runprompt a.prompt | ./runprompt b.prompt
Cached responses are stored in ~/.cache/runprompt/ (or $XDG_CACHE_HOME/runprompt/), based on the inputs applied to the template and frontmatter.
See --help for more information.
Spec compliance
This is a minimal implementation of the Dotprompt specification. Not yet supported:
- Multi-message prompts (
{{role}},{{history}}) - Helpers (
{{json}},{{media}},{{section}}) - Model config (
temperature,maxOutputTokens, etc.) - Partials (
{{>partialName}}) - Nested Picoschema (objects, arrays of objects, enums)
The YAML parser is a minimal implementation that handles only the subset of YAML needed for Dotprompt frontmatter (simple key-value pairs, nested objects, and lists). It will likely fail on complex YAML features like anchors, multi-line strings, or flow syntax. See optional dependencies below to use pyyaml instead.
Optional dependencies
You can install optional dependencies for enhanced features:
pyyaml: For full YAML spec support in frontmatter.playwright: For high-quality web scraping via thebuiltin.fetch_cleantool.
If you installed via pip or uv, you can get these with:
pip install "runprompt[full] @ git+https://github.com/chr15m/runprompt.git"
If PyYAML is installed, it will be used automatically instead of the minimal internal parser.