Tools · LLM application security · 06 / 23
Log a suspected injection without the prompt
When a turn looks like an injection, store the channel, the source, a hash, and a short redacted excerpt. Leave the full prompt out if it may hold secrets.
The prompt is often the sensitive part
A suspect turn is worth recording. The people who review it need to know which assistant, which channel, and whether a tool was blocked. They do not automatically need the whole prompt. The prompt holds the user’s question, the retrieved passage, and any tool result. Those spans can include customer names, mailbox contents, or a secret someone pasted into a wiki.
Impact of storing the raw prompt: the log becomes a second copy of that secret, often with a wider set of readers than the wiki had. Control: store enough to find the source and to show the decision, and redact or omit the rest.
A record that still explains the decision
Keep the time, the assistant name, the channel (user, retrieval, or tool), and a source id that points back to the document or the tool call. Hash the suspect span so two reviewers can tell whether they are looking at the same text without passing the text around. Store the length. If a tool was requested, store the tool name and the allow-or-block decision.
Cap any excerpt at 120 characters, and only after a redactor has run. If that pass replaces a secret-shaped token, drop the excerpt and keep the hash. A clean excerpt with no replacement can stay. If you would still not paste the remainder into a ticket, skip it.
function event({ assistant, channel, source, span, tool, decision }) {
const excerpt = redact(span).slice(0, 120)
const safe = excerpt.includes('[redacted]') ? '' : excerpt
return {
assistant, channel, source,
span_sha256: sha256(span),
span_chars: span.length,
excerpt: safe,
tool: tool || null,
decision,
}
}Redact before the row is written
Run the redactor in the application, before the logging client. Do not send the raw span to a log vendor and hope a downstream rule catches it. A simple pass replaces long unbroken tokens. Add patterns your organization already uses for secret scanning, and test them with fake values in a unit test, not with a live credential.
The hash is of the original span, computed locally, so you can match a later copy of the same document. Matching does not require the log to hold the span.
Worked case: a password pasted into a wiki page
A facilities page includes a database password a contractor pasted under a heading. Harbor Desk retrieves the page, and the model starts to propose opening a connection with that value. The operator flags the turn. The tool open_connection is not on the allowlist, so the call is blocked before any connection is attempted.
The log row names the assistant, the channel “retrieval,” the wiki page id, the tool name, and the decision “blocked.” The excerpt is empty because the redactor found a secret-shaped token, and the row stores the hash and the character count instead. The password is not in the log. The follow-up is outside the model: remove the password from the wiki, rotate it, and confirm the page no longer matches the hash.
{
"assistant": "harbor-desk",
"channel": "retrieval",
"source": "wiki:page-1842",
"span_sha256": "<hash computed locally>",
"span_chars": 640,
"excerpt": "",
"tool": "open_connection",
"decision": "blocked"
}Diagram
What the injection log keeps
The row should let a reviewer find the source and the decision.
- Keep
Time, assistant, channel, source id, tool name, and allow-or-block.
- Hash
A local hash of the span, plus its length, so the text can stay out of the row.
- Excerpt
At most a short redacted slice, and nothing if a secret pattern matched.
- Leave out
The full prompt, retrieved customer text, and any live credential.
Check yourself
1. Why avoid storing the full prompt in the injection log?
2. What do you keep so two reviewers can tell it is the same span?
3. When should the excerpt be left empty?