Skip to content

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:

> compare @src/before.py with @src/after.py: what actually changed?

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.com stays 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:

> !git status
> !cargo test 2>&1 | head -40

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:

> !!git fetch origin

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:

> /review @src/auth.py

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 as skill_<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.