Shell Context Injection in Templates¶
Inject live system context into prompt templates by running shell commands at render time. The !cmd`` syntax executes a command and replaces itself with stdout.
from promptise.prompts import TemplateEngine, SubprocessShellExecutor
engine = TemplateEngine(
shell_executor=SubprocessShellExecutor(timeout=2.0),
)
out = engine.render(
"Today is !`date +%Y-%m-%d`. Branch: !`git rev-parse --abbrev-ref HEAD`.",
{},
)
# "Today is 2026-04-11. Branch: main."
Disabled by default¶
Without an explicit shell_executor, the !cmd`` syntax is left as literal text — there is no implicit shell access, ever. This is a deliberate security choice.
# No executor → syntax stays literal
engine = TemplateEngine()
out = engine.render("Today is !`date`", {})
# "Today is !`date`"
SubprocessShellExecutor¶
The built-in executor runs commands via the system shell with a configurable timeout:
from promptise.prompts import SubprocessShellExecutor
executor = SubprocessShellExecutor(
timeout=5.0, # max seconds per command (default 5)
shell=True, # run via /bin/sh -c (default True)
cwd="/path/to/repo", # working directory (default: inherited)
env={"API_KEY": "x"}, # extra env vars (merged with os.environ)
allowlist={"echo", "date", "git"}, # restrict allowed commands
)
With shell=False the command line is tokenized like a terminal would
(shlex.split) and its executable resolved on PATH. Unless cwd is set,
the process is started without fork() (CPython's posix_spawn path): a
forked child of a multithreaded process that has used macOS system frameworks
— any httpx client does — can die with SIGSEGV before exec, which surfaces
as exited -11. Setting cwd forces the fork path, so prefer running the
command with an absolute path over changing directory.
Allowlist¶
When allowlist is set, only commands whose first token matches are permitted:
executor = SubprocessShellExecutor(allowlist={"echo", "git"})
# Works
engine = TemplateEngine(shell_executor=executor)
engine.render("!`echo hello`", {})
# Raises ShellExecutionError
engine.render("!`rm -rf /`", {})
Custom executors¶
Any callable (str) -> str works as a shell executor:
# Dry-run executor for testing
engine = TemplateEngine(shell_executor=lambda cmd: f"[would run: {cmd}]")
# Sandbox executor
async def sandboxed(cmd: str) -> str:
return await sandbox.execute(cmd)
Processing order¶
Shell interpolation runs after template includes and before conditionals and variable substitution. This means:
{% include "name" %}— resolved first (included templates can contain!cmd``)!cmd`` — executed, stdout replaces the block{% if %},{% for %}— conditionals and loops{variable}— variable interpolation
Error handling¶
- Non-zero exit code → raises
ShellExecutionError - Timeout → raises
ShellExecutionError - Command not in allowlist → raises
ShellExecutionError
All errors propagate to the caller — there is no silent fallback.
Related¶
- Loader & Templates — the full template engine reference
- Context & Variables — dynamic context injection via providers