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

> Learn to use prompt argument substitution with {{KEY}} placeholders in Sandcastle. Inject runtime values via --prompt-args for dynamic LLM agent interaction.

- Repository: [Matt Pocock/sandcastle](https://github.com/mattpocock/sandcastle)
- Tags: how-to-guide
- Published: 2026-05-24

---

**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)](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)](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:

```bash
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:

```bash
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:

```bash
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:

```typescript
// 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)](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)](https://github.com/mattpocock/sandcastle/blob/main/src/run.ts`), which wires CLI options to [`PromptResolver.ts`](https://github.com/mattpocock/sandcastle/blob/main/PromptResolver.ts) and [`PromptArgumentSubstitution.ts`](https://github.com/mattpocock/sandcastle/blob/main/PromptArgumentSubstitution.ts).

## Frequently Asked Questions

### Can I use prompt argument substitution with inline prompts?

No. The `validateNoArgsWithInlinePrompt` function in [`src/PromptArgumentSubstitution.ts`](https://github.com/mattpocock/sandcastle/blob/main/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.