How to Use Prompt Argument Substitution with `{{KEY}}` Placeholders in Sandcastle

Sandcastle supports dynamic placeholder substitution using {{KEY}} syntax in prompt files, allowing you to inject runtime values via the --prompt-args CLI option before sending the prompt to the LLM agent.

The prompt argument substitution feature in mattpocock/sandcastle enables you to write reusable prompt templates with variables like {{ISSUE_NUMBER}} that get replaced at runtime. This functionality only works with prompt files referenced via --prompt-file, not with inline prompts passed directly on the command line.

How Prompt Argument Substitution Works

The substitution pipeline follows a strict validation and resolution flow defined across several core modules.

Step 1: Prompt Source Resolution

The system first determines where the prompt originates. In [src/PromptResolver.ts](https://github.com/mattpocock/sandcastle/blob/main/src/PromptResolver.ts), the resolvePrompt function distinguishes between inline prompts (passed via --prompt) and template prompts (loaded from --prompt-file). Only template sources are eligible for argument substitution.

Step 2: Validation Guards

Before substitution occurs, Sandcastle enforces two critical validation rules in [src/PromptArgumentSubstitution.ts](https://github.com/mattpocock/sandcastle/blob/main/src/PromptArgumentSubstitution.ts):

  • validateNoArgsWithInlinePrompt: Throws a PromptError if you supply --prompt-args while using an inline prompt, preventing silent failures where substitution would be ignored.
  • validateNoBuiltInArgOverride: Blocks attempts to override reserved keys (SOURCE_BRANCH, TARGET_BRANCH) via user-supplied arguments, as these are injected automatically by the system.

Step 3: Placeholder Detection and Replacement

The substitutePromptArgs function handles the actual text transformation:

  1. Sanitization: Removes internal shell-block markers from arguments to prevent injection issues.
  2. Missing Key Detection: Uses findMissingPromptArgKeys with the PLACEHOLDER_PATTERN regex to scan for {{KEY}} patterns and verify all placeholders have corresponding values in the promptArgs object. Missing keys trigger a PromptError.
  3. Substitution: Replaces each {{KEY}} occurrence with args[KEY].toString().
  4. Unused Key Warnings: Emits CLI warnings for any supplied arguments not present in the template, unless the key is listed in the optional silentKeys set (used internally for built-in arguments).

Using {{KEY}} Placeholders in Practice

To implement prompt argument substitution, you must store your template in a file and reference it when running Sandcastle.

Creating a Prompt Template

Create a markdown file containing your placeholders:

cat > .sandcastle/prompt.md <<'EOF'
The issue number is {{ISSUE_NUMBER}}.
Please summarize the changes in branch {{SOURCE_BRANCH}}.
EOF

Running with Arguments

Pass values using the --prompt-args flag with KEY=value syntax:

sandcastle run \
  --prompt-file .sandcastle/prompt.md \
  --prompt-args ISSUE_NUMBER=42

When executed, resolvePrompt identifies the template source, validation passes, and substitutePromptArgs replaces {{ISSUE_NUMBER}} with 42. If {{SOURCE_BRANCH}} isn't provided and isn't automatically injected, the operation fails with a missing key error.

Leveraging Built-in Arguments

Sandcastle automatically injects Git context via built-in keys when running from a worktree:

  • SOURCE_BRANCH: The current Git branch
  • TARGET_BRANCH: The target branch for comparison

These require no --prompt-args entries:

sandcastle run \
  --prompt-file .sandcastle/prompt.md \
  --prompt-args ISSUE_NUMBER=108

If the current branch is feature/login and the target is main, the final prompt becomes:


The issue number is 108.
Please summarize the changes in branch feature/login.

Handling Validation and Warnings

The substitution system includes strict error handling to ensure template integrity.

Missing Placeholder Values

If a template contains {{KEY}} but the key is absent from promptArgs and not built-in, findMissingPromptArgKeys returns the missing keys and Sandcastle throws a PromptError before contacting the LLM.

Unused Argument Warnings

Supplying extra arguments not present in the template triggers a CLI warning. To suppress this for specific keys (as done internally for built-ins), the substitutePromptArgs function accepts an optional silentKeys Set:

// Internal usage example from the codebase
substitutePromptArgs(
  template,
  { ISSUE_NUMBER: "7", UNUSED: "value" },
  new Set(["UNUSED"])   // silences warning for UNUSED
);

Shell Block Integration

The preprocessor in [src/PromptPreprocessor.ts](https://github.com/mattpocock/sandcastle/blob/main/src/PromptPreprocessor.ts` interacts with the substitution logic by marking shell blocks (!...`) so the system can distinguish between template placeholders and executable shell commands extracted from the prompt.

Summary

  • Prompt argument substitution only works with --prompt-file, not inline --prompt strings, as enforced by validateNoArgsWithInlinePrompt.
  • Placeholders use the {{KEY}} syntax scanned by the PLACEHOLDER_PATTERN regex in substitutePromptArgs.
  • Built-in keys SOURCE_BRANCH and TARGET_BRANCH are reserved and injected automatically; attempting to override them via --prompt-args causes a PromptError.
  • Missing placeholder values trigger immediate errors, while unused arguments generate warnings unless silenced via the silentKeys parameter.
  • The orchestration happens in [src/run.ts](https://github.com/mattpocock/sandcastle/blob/main/src/run.ts`), which wires CLI options to PromptResolver.ts and PromptArgumentSubstitution.ts.

Frequently Asked Questions

Can I use prompt argument substitution with inline prompts?

No. The validateNoArgsWithInlinePrompt function in src/PromptArgumentSubstitution.ts explicitly throws a PromptError if you supply --prompt-args while using the --prompt flag. Substitution requires a template file source so that the system can scan for {{KEY}} patterns before sending the text to the agent.

Why am I getting an error about SOURCE_BRANCH or TARGET_BRANCH?

These are built-in arguments that Sandcastle injects automatically based on your Git context. The validateNoBuiltInArgOverride function prevents you from setting these via --prompt-args to avoid conflicts. Remove these keys from your --prompt-args and let the system populate them from the repository state.

What happens if I provide extra arguments that don't exist in the template?

Sandcastle emits a warning in the CLI output indicating that certain arguments were unused. This helps catch typos in placeholder names. To suppress these warnings for specific keys (as done internally for built-ins), the substitutePromptArgs function accepts a silentKeys Set parameter that filters warnings for approved keys.

How does Sandcastle handle missing placeholder values?

Before substitution, findMissingPromptArgKeys compares the template's {{KEY}} occurrences against the supplied promptArgs object. If any placeholders lack corresponding values (and aren't built-in keys), Sandcastle throws a PromptError immediately, preventing incomplete prompts from reaching the LLM.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →