Skip to content

Harden prompt template substitution (B10, B15, B16, F7) - #367

Merged
andrewklingelhofer merged 3 commits into
mainfrom
fix/audit-prompt-template-safety
Jul 18, 2026
Merged

Harden prompt template substitution (B10, B15, B16, F7)#367
andrewklingelhofer merged 3 commits into
mainfrom
fix/audit-prompt-template-safety

Conversation

@andrewklingelhofer

Copy link
Copy Markdown
Contributor

Summary

The custom/skill prompt substitution interpolated untrusted issue text unsafely.

# Problem Fix
B10 Issue values used as String.replace replacement$&, $`, $', $$ reinterpreted, corrupting prompts Single-pass function-replacement substitute(content, vars) (shared by both former loops)
B15 Multi-pass loop let one value inject another's {{token}} Eliminated by the single pass
B16 {{description}} was raw for custom types while builtins stripped repo/branch directive lines Clean {{description}} via the same strip helpers
F7 Unknown {{tokens}} passed through silently Warn on keys left unresolved during the substitution scan (no false positive when a resolved value legitimately contains {{…}})

Tests

audit-prompt-safety.test.ts$-sequences inserted literally; cross-token injection prevented; description cleaned; genuinely-unknown token warns while a literal {{…}} inside a value does not.

Verification

typecheck clean · lint exit 0 · bun test 849 pass / 0 fail.

🤖 Generated with Claude Code

andrewklingelhofer and others added 3 commits June 15, 2026 21:46
Replace the two ad-hoc per-key substitution loops in resolvePrompt() and
resolveSkills() with a single shared single-pass substitute() helper:

- B10: use a replacer function so untrusted issue values containing regex
  replacement specials ($&, $`, $', $$) are inserted literally instead of
  being interpreted.
- B15: a single pass means one variable's value can no longer inject another
  variable's {{token}} on a later pass.
- B16: {{description}} is now cleaned with the same stripBranchLine/stripRepoLine
  helpers used by builtin planning/review, so custom prompts get repo:/branch:
  directive lines stripped. The unmodified text is exposed as {{descriptionRaw}}.
- F7: residual unknown {{token}}s are detected after substitution and logged
  via logWarn (named), without throwing.

Adds src/__tests__/audit-prompt-safety.test.ts covering literal insertion of
replacement specials, no cross-variable token injection, description cleaning
for custom types, and the unknown-token warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
warnResidualTokens re-scanned the substituted output, so a resolved
variable whose value legitimately contains literal "{{description}}"
(the B15 scenario) triggered a spurious unresolved-token warning. Collect
unknown keys during the single substitute() pass instead, so a token that
came from an inserted value never warns. Also reword the description-cleaning
comment to note it affects all prompt paths (builtin skills, custom prompts,
cli-prompt-render).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
resolvePhaseMcpConfig expands ~ via node:os homedir(); the test compared it to
process.env.HOME. Bun caches homedir() on first call and ignores later
process.env.HOME mutations, so when a sibling test changes HOME they diverge on
Linux CI and this test fails (works on macOS by execution-order luck). Assert
against the same homedir() the implementation uses.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@andrewklingelhofer
andrewklingelhofer merged commit af30a30 into main Jul 18, 2026
1 check passed
@andrewklingelhofer
andrewklingelhofer deleted the fix/audit-prompt-template-safety branch July 18, 2026 17:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant