title: Input surfaces
description: @path, !cmd, and reusable prompt templates: three ways to shape what the model sees, without pasting.
Input surfaces¶
Since v0.7, letscode preprocesses user input on its way to the model. Three shapes:
@path: inline a file's contents.!cmd: inline the output of a shell command./<name> args: expand a reusable prompt template.
All three work in interactive mode, one-shot (letscode "…"), and print mode (letscode -p). Slash commands are dispatched first, so /help and friends still take precedence over @path / !cmd expansion.
@path: inline a file¶
Anywhere in your message, a whitespace-delimited token starting with @ is treated as a filename. The file is read (utf-8, capped at 32 KiB) and spliced in with clear delimiters:
Rules and edge cases:
- Paths are resolved against
cwd, so relative paths work naturally. ~expands (@~/notes/foo.md).- Binary files return
[@name: binary file, N bytes not inlined], so the prompt stays intact. - Missing files return
[@name: not found]so the model sees why the reference is empty. - Files larger than 32 KiB are truncated with a
... (truncated)marker. - Tokens starting with
@at a word boundary only;user@host.comstays literal.
!cmd: inline shell output¶
When a line starts with a single !, the rest of the line is run as a shell command via letscode's ExecutionEnv (same env the built-in bash tool uses). The exit code and merged stdout/stderr are appended to the message:
A double-bang !!cmd runs for side effects only; output is discarded and, if that was the entire message, nothing is sent to the model:
Bounded by a 30-second timeout; 32 KiB of output cap. Failures show as [!cmd: <reason>].
Sandbox and !cmd
!cmd runs against the local LocalEnv; it never touches any sandboxed execution env you may have swapped in for tool calls. ! is user-typed input, so the user is in charge; the sandbox is meant to protect against the LLM. If you need !cmd under a sandbox, file an issue. The seam is there, just not threaded.
Prompt templates: /<name> args¶
Drop a Markdown file at ~/.letscode/prompts/<name>.md and typing /<name> <args> in the prompt splices its body into the next user message (via the same CommandResult.send_to_llm path skills use). The literal token {{args}} in the body is replaced with everything after the command name.
Example: ~/.letscode/prompts/review.md:
Please review the following change and flag anything that will bite us
in production: correctness bugs, missing invariants, over-defensive
coding, unnecessary abstractions.
{{args}}
Then:
The template body is expanded first, then @path expansion runs on the result. So templates can reference {{args}} that themselves contain @path tokens.
Discovery¶
The same four-way discovery paths as skills (user-global first, then project-local; closest project wins on name collision):
| Scope | Paths (in order) |
|---|---|
User (under $HOME) |
.letscode/prompts/, .agents/prompts/, .pi/agent/prompts/, .claude/prompts/ |
Project (walked from cwd) |
.letscode/prompts/, .agents/prompts/, .pi/prompts/, .claude/prompts/ |
Templates whose stem collides with a built-in or plugin command are skipped with a warning. Never overwriting a slash command you already have.
Templates vs skills¶
Two ways to reach the model with reusable prose:
- Skill (
SKILL.md+ YAML frontmatter). Registered asskill_<name>tool so the model can invoke it. Also available as/skill:<name>for the user. - Prompt template (
<name>.md, no frontmatter). Registered as/<name>for the user to invoke. Not visible to the model as a tool.
Reach for a prompt template when the trigger is human ("I want to run my review checklist"); reach for a skill when the trigger is the task ("when reviewing code, follow these steps"). Both formats interoperate with the same .agents/ / .claude/ / .pi/ ecosystems.
Why these three, and not a bigger DSL¶
Every shape here has an obvious equivalent the user could type out: cat this file, run this command, paste this template. The preprocessor removes friction without hiding anything: what gets sent to the model is a plain expanded message, no hidden state, no separate escape grammar to remember. If none of the three fits, use a plugin.