Home / AI & GenAI

AI & GenAITroubleshootingIntermediate

Amazon Bedrock Guardrails block legitimate prompts: reading the trace, tuning filter strengths and testing with ApplyGuardrail

Users get the blocked message for normal questions. Find which Bedrock Guardrails policy fired, reproduce it with ApplyGuardrail, and tune it without weakening protection.

Deepak Bhardwaj8 min read

Applies toAmazon Bedrock Guardrails, Converse/ConverseStream and ApplyGuardrail APIs, AWS CLI v2

Symptom

Users of a Bedrock-backed assistant get your guardrail's blocked message ("Sorry, I can't help with that") for ordinary questions. Typical reports:

  • A support agent asks how to "kill a stuck process" and gets blocked.
  • A finance user asks about retirement planning and the guardrail refuses it as investment advice.
  • Answers come back with {NAME} or {EMAIL} placeholders where the user expected real values.
  • Long prompts that paste in logs or tickets are blocked, but short ones aren't.

The Converse response has "stopReason": "guardrail_intervened". That tells you a guardrail acted. It doesn't tell you which policy fired, or whether it blocked or masked. The trace tells you that.

Quick triage table

What you see Likely cause Fix
Blocked on words like "kill", "attack", "exploit" in an IT context Content filter VIOLENCE or MISCONDUCT set to HIGH Lower the input strength for that category, re-test
Blocked on pasted logs, code or instructions PROMPT_ATTACK filter flags pasted text as an injection attempt Wrap only the user's question in guardContent, not the whole pasted document
Blocked on a topic close to a denied one Denied topic definition too broad, or the examples overlap allowed use Tighten the definition, add sample phrases that only match the denied use
Values replaced with {EMAIL} etc. Sensitive information filter set to anonymize (mask) Change the action for that entity type, or accept the masking
Answer blocked after the model replied Output assessment, often contextual grounding below threshold Check contextualGroundingPolicy score against threshold
Exact term always blocked Custom word filter or managed profanity list Remove the word from the word policy

Step 1: Get the trace from the failing call

Turn on the trace in the Converse call that is failing. In Python with boto3:

import boto3, json

brt = boto3.client("bedrock-runtime", region_name="us-east-1")
MODEL_ID = "<your-model-id>"

resp = brt.converse(
    modelId=MODEL_ID,  # the model ID or inference profile your app already uses
    messages=[{"role": "user", "content": [{"text": "How do I kill a hung java process on Linux?"}]}],
    guardrailConfig={
        "guardrailIdentifier": "abc123xyz",
        "guardrailVersion": "3",
        "trace": "enabled",
    },
)

print(resp["stopReason"])
print(json.dumps(resp.get("trace", {}).get("guardrail", {}), indent=2, default=str))

Look for inputAssessment (the prompt was blocked) or outputAssessments (the model's answer was blocked). Inside each assessment, find the entries with "action": "BLOCKED" or "ANONYMIZED":

  • contentPolicy.filters[] gives type (HATE, INSULTS, SEXUAL, VIOLENCE, MISCONDUCT, PROMPT_ATTACK), confidence and action.
  • topicPolicy.topics[] gives the denied topic name.
  • wordPolicy.customWords[] and managedWordLists[] give the match.
  • sensitiveInformationPolicy.piiEntities[] and regexes[] give the entity type and the matched text.
  • contextualGroundingPolicy.filters[] gives score against threshold for GROUNDING and RELEVANCE.

Warning

The trace contains the matched text, including PII the guardrail masked. Don't send traces to general application logs. Enable trace only for debugging, or send it to a restricted log destination.

Step 2: Reproduce it with ApplyGuardrail

ApplyGuardrail evaluates text against a guardrail without calling a model. Use it to replay the exact user text quickly and cheaply:

aws bedrock-runtime apply-guardrail \
  --guardrail-identifier abc123xyz \
  --guardrail-version 3 \
  --source INPUT \
  --content '[{"text":{"text":"How do I kill a hung java process on Linux?"}}]' \
  --output-scope FULL \
  --region us-east-1 \
  --query '{action:action,reason:actionReason,assessments:assessments}'
  • --source INPUT checks text as a user prompt. Use OUTPUT to check a model answer.
  • --output-scope FULL returns entries that were evaluated but not detected, as well as the ones that fired. It helps you see how close a prompt is to the threshold. Per the CLI reference, full scope doesn't cover word filters or regex sensitive-information filters.
  • action is GUARDRAIL_INTERVENED or NONE.

To see the configured strengths you are comparing against:

aws bedrock get-guardrail \
  --guardrail-identifier abc123xyz \
  --guardrail-version 3 \
  --query '{status:status,content:contentPolicy.filters,topics:topicPolicy.topics[].name}'

Content filter strengths are NONE, LOW, MEDIUM and HIGH, set separately for input and output. A filter blocks when the detected confidence reaches the configured strength. HIGH strength blocks even low-confidence detections, so it is the most likely cause of false positives.

Step 3: Fix the cause

Content filters too strict

If VIOLENCE or MISCONDUCT fires on IT vocabulary at LOW confidence, lower the input strength for that category from HIGH to MEDIUM. Leave the output strength as it is unless output answers are also blocked. Change one category at a time.

Prompt attack filter catching pasted content

The PROMPT_ATTACK filter is designed to catch jailbreaks and instruction injection. Pasted runbooks, tickets and logs often contain phrases like "ignore the previous error" or "you must run", which look like injection. Don't switch the filter off. Instead, tell the guardrail which part of the message is the user's own input. Once a message contains a guardContent block, the guardrail evaluates only the content inside guardContent blocks:

messages = [{
    "role": "user",
    "content": [
        {"guardContent": {"text": {"text": user_question}}},   # evaluated
        {"text": f"Reference material:\n{retrieved_docs}"},     # not evaluated
    ],
}]

Caution

Content outside guardContent is not checked at all. If the retrieved or pasted text comes from untrusted sources, you have removed the check that protects against indirect prompt injection. Scan untrusted documents separately with apply-guardrail --source INPUT at ingestion time, or keep them inside guardContent and accept some false positives.

The system prompt is not evaluated unless you put it in a guardContent block. Tool definitions, tool call arguments and tool results are not evaluated by Converse guardrails either. Check tool output yourself if it matters.

Denied topic too broad

A topic like "Investment advice" with the definition "Any discussion of money, savings or finance" blocks expense policy questions too. Write the definition around the behaviour you want to stop ("Recommendations to buy, sell or hold specific securities or funds") and give sample phrases that only match that behaviour. Test both a denied and an allowed phrase with apply-guardrail after every change.

Sensitive information masked unexpectedly

ANONYMIZED in the trace means the guardrail masked the value rather than blocking it. stopReason is still guardrail_intervened, so apps that treat every intervention as a refusal show the blocked message when the answer was only masked. Check the action field in the trace, not only stopReason.

Step 4: Ship the change safely

Guardrail edits go to the DRAFT version. Production should call a numbered version, so a change only takes effect when you publish and switch to it.

  1. Edit the guardrail in the console, or with aws bedrock update-guardrail. The update call takes the full policy set, so start from the current get-guardrail output and change only what you need.
  2. Re-run your test set against DRAFT:
while IFS= read -r line; do
  result=$(aws bedrock-runtime apply-guardrail \
    --guardrail-identifier abc123xyz --guardrail-version DRAFT \
    --source INPUT \
    --content "$(jq -cn --arg t "$line" '[{text:{text:$t}}]')" \
    --query action --output text)
  printf '%s\t%s\n' "$result" "$line"
done < guardrail-tests.txt

Keep two lists in guardrail-tests.txt (or two files): prompts that must pass and prompts that must be blocked. A fix that unblocks "kill a process" but also unblocks a real jailbreak is not a fix.

  1. Publish a new version and point the app at it:
aws bedrock create-guardrail-version \
  --guardrail-identifier abc123xyz \
  --description "Lower VIOLENCE input strength to MEDIUM, ticket 4821"
  1. Update the app config to the returned version number. Rolling back means pointing the app at the previous version number, because published versions don't change.

Tip

Keep the guardrail version in app configuration (Parameter Store, AppConfig, environment variable), not in code. You can roll back without redeploying.

Prevent it next time

  • Keep a regression set of real allowed and blocked prompts per guardrail, and run it before every create-guardrail-version.
  • Start content filters at MEDIUM for internal tools and raise individual categories only when you have evidence.
  • Use guardContent deliberately: decide for each message part (user input, system prompt, retrieved documents) whether it must be evaluated, and write that down.
  • Count interventions by policy type. Send trace data for blocked calls to a restricted log group and alert on a sudden rise in one category after a guardrail or prompt change.
  • Handle ANONYMIZED and BLOCKED separately in the app so masked answers aren't shown as refusals.
  • Give users a "this was blocked wrongly" button that records the request ID, so false positives reach you as tickets instead of complaints.