PromptisePromptise
Docs
GitHubGet started
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> Guardrails and security

Stop Secrets Leaking Into LLM Logs and Traces

How API keys and tokens end up in LLM logs and traces, and how to redact them from tool results, observability and log lines before anything is stored.

Level
Intermediate
Reading time
18 min
Published
Oct 22, 2026
Last updated
Oct 22, 2026
Tested with
Promptise 1.6.0
By
Promptise Team
  • System Prompt Leakage
  • Observability
  • Guardrails
  • Tool Calling
Contents · 14 sections↓
  1. 01How do you stop secrets leaking into LLM logs and traces?
  2. 02How do secrets end up in LLM logs and traces?
  3. 03What does Promptise Foundry redact by default?
  4. 04When to redact, and when not to record at all
  5. 05What you need
  6. 06Step 1: Build a tool that leaks
  7. 07Step 2: See where secrets go with default settings
  8. 08Step 3: Redact tool results with a guardrail
  9. 09Step 4: Close the gaps in traces and your own log lines
  10. 10Running it in production
  11. 11Common problems and fixes
  12. 12Limitations
  13. 13Frequently asked questions
  14. 14Next steps

Secrets leak into LLM logs and traces more often through tool results than through prompts. An agent runs a CLI, reads a config file or calls an API, the output contains a token, and request logging, tracing and the conversation store all keep a copy. This guide shows where API keys and tokens end up, what to redact and where, and how to do it in Python with Promptise Foundry 1.6.0, including the token formats the built-in redaction misses.

[01]

How do you stop secrets leaking into LLM logs and traces?

Redact in two layers. First, scan every tool result for credentials and replace them before the model reads the result, so the secret never reaches the provider, the conversation history or anything downstream. Second, put a sanitizer on everything you record (traces, exports and your own log lines) to catch what the first layer missed, such as secrets in tool arguments or error messages. Then test both layers with fake keys in every format your tools can return.

  • Tool results are the main path. They hold raw CLI output, HTTP responses and file contents, and agents pass them on verbatim.

  • A secret the model sees is everywhere. It is sent to the provider and kept in every store that saves the conversation.

  • Trace redaction alone is too late. It cleans the trace, but the model and the history already have the value.

  • Pattern lists have gaps. In Foundry 1.6.0 the trace sanitizer misses GitHub CLI tokens (gho_), and the guardrail misses opaque bearer tokens. Add rules for your formats.

  • Not recording is safer than redacting. Turn off tool input and output recording when traces leave your trust boundary.


[02]

How do secrets end up in LLM logs and traces?

An agent's text moves through more places than a normal request handler. A secret can enter at several points:

  • Tool results. A shell tool prints environment variables, an HTTP tool returns response headers, a file tool reads .env. The agent passes the whole result to the model.

  • Tool arguments. The model copies a token from one tool result into the next call's arguments, for example a curl command with an Authorization header.

  • User messages. People paste keys into chat, often while asking for help with an error.

  • Error messages. Exceptions from HTTP clients and database drivers can include URLs with passwords or request details.

  • Proxy and gateway logs. A gateway that logs request and response bodies keeps every prompt and every tool result that passes through it.

LiteLLM's incident post of October 21, 2026 is a recent example of the last path. LiteLLM says that in specific situations Claude Code embeds a user's GitHub CLI OAuth token in a response body, and where response bodies are logged, those tokens may appear in LiteLLM logs. It believes the attacker obtained developer GitHub tokens from an internal LiteLLM deployment. In March, LiteLLM had reported a separate issue in which a custom guardrail could write plaintext Authorization headers into spend logs and OpenTelemetry traces. Neither needed an exotic attack: secrets were in text that a logging feature saved.

A tool result such as CLI output, an API response or a file passes through layer 1, a scan that redacts credentials, before it reaches the model context and provider, the conversation store and memory, traces and exports, and your own log lines. Layer 2, a trace sanitizer and log filter, also scrubs tool arguments, errors and prompts before they reach traces and logs.
FigureWhere a secret in a tool result ends up, and the two places to redact it

The order matters. Once a secret is in the model's context, cleaning the trace does not take it back from the provider or from the stored conversation. Redact as early as you can, and keep a second layer at the point where text is written down.


[03]

What does Promptise Foundry redact by default?

Foundry has two separate redaction features. They use different pattern lists, and only one is on by default.

Layer

On by default

Where it runs

What it catches

Trace sanitizer (ObservabilityConfig.redact_sensitive)

Yes, when observability is on

Every recorded event: tool arguments, tool result previews, errors, prompt text

sk- keys (OpenAI, Anthropic), AWS access key IDs, classic GitHub tokens (ghp_), Bearer tokens, passwords in URLs, card numbers, US SSNs, emails

Guardrail credential detector (CredentialDetector)

No; tool results only with scan_tool_results=True

Tool results before the model, and the agent's output

96 patterns in about 60 groups (cloud, Git hosts, payment, messaging, databases, JWTs, private keys), adapted from gitleaks and trufflehog

Three things to know about these defaults:

  • The trace sanitizer cleans the record, not the run. The model and result["messages"] still hold the raw tool result.

  • Tool results are not scanned unless you ask. PromptiseSecurityScanner scans tool results only with scan_tool_results=True. On user input, credential findings are warnings unless you set redact_input=True.

  • Recording is on by default. record_tool_io=True stores tool arguments and the first 2,000 characters of each result. Prompt and response text is recorded only at the FULL level.


[04]

When to redact, and when not to record at all

Redaction is pattern matching. It catches formats it knows and misses the rest, such as a random 32-character password in a config file. Use the stronger control where you can:

Situation

Do this

Traces are sent to a third-party service

record_tool_io=False, keep prompts off, and redact anyway

You debug locally and need real arguments

Keep recording on, with redaction, and keep traces on your machine

A tool can return credentials by design (secret managers, env dumps)

Don't give the agent that tool; give it a narrower one

Tools return text from systems you don't control

Scan tool results (layer 1) and sanitize traces (layer 2)

Logs go to a shared logging platform

Log metadata (tool name, status, latency), not bodies

The best fix is upstream: an agent that never has a token cannot leak it. Foundry's MCP server specs pass bearer_token, headers and env to tool servers without showing them to the model; see AI agent secrets management.


[05]

What you need

  • Python 3.10 or later and promptise==1.6.0.

  • An OpenAI API key in OPENAI_API_KEY. The examples use openai:gpt-5-mini, and each run costs well under a cent.

  • About 20 minutes.

>_Terminal
uv venv && uv pip install "promptise==1.6.0"

[06]

Step 1: Build a tool that leaks

To test redaction, reproduce a leak with values you control. This tool returns the kind of debug output a deploy CLI prints, with five fake secrets in real formats: an OpenAI project key, an AWS access key ID, a GitHub CLI OAuth token, a database URL with a password and an opaque bearer token.

Pythonleaky_tool.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
"""A tool whose output contains secrets, the way real CLI and API tools leak them.

Every value below is fake, but each has the real format: an OpenAI project key,
an AWS access key ID, a GitHub CLI OAuth token (gho_), a database URL with a
password, and an opaque bearer token in an Authorization header.
"""
from promptise import tool

FAKE_OUTPUT = """\
deploy: api-prod is healthy (3/3 pods), last release v2.14.1
debug env:
  OPENAI_API_KEY=sk-proj-FAKEfakeFAKEfakeFAKE12T3BlbkFJFAKEfakeFAKEfakeFAKE34
  AWS_ACCESS_KEY_ID=AKIAFAKE1234567890AB
  GH_TOKEN=gho_FAKEaaaabbbbccccddddeeeeffffgggg1234
  DATABASE_URL=postgres://app:FakePassw0rd@db.internal:5432/app
request: Authorization: Bearer 7f3cFAKE9a1e5b2d4f6a8c0e1b3d5f7a
"""

SECRETS = {
    "openai key": "sk-proj-FAKE",
    "aws key": "AKIAFAKE",
    "github cli token": "gho_FAKE",
    "db password": "FakePassw0rd",
    "bearer token": "7f3cFAKE",
}


@tool
async def deploy_status(service: str) -> str:
    """Get the deploy status of a service, with the CLI's debug output.

    Args:
        service: The service name, for example "api-prod".
    """
    return FAKE_OUTPUT


def leaks(text: str) -> list[str]:
    """Which of the fake secrets appear in text."""
    return [name for name, marker in SECRETS.items() if marker in text]

leaks() reports which fake secrets appear in a piece of text. The tests below use it on the messages the model received and on the trace log file.


[07]

Step 2: See where secrets go with default settings

A small helper runs the agent once, then checks two places: the tool message the model received and the JSON log file written by the structured log transporter.

Pythoncommon.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"""Run the agent once and report where the fake secrets ended up."""
import json
from pathlib import Path

from leaky_tool import leaks

QUESTION = "Is api-prod healthy? Reply with healthy or unhealthy and the pod count, nothing else."


async def run_and_report(agent, log: Path) -> None:
    result = await agent.ainvoke({"messages": [{"role": "user", "content": QUESTION}]})
    await agent.shutdown()  # flushes the log transporter

    tool_msg = next(m for m in result["messages"] if m.type == "tool")
    traced = [json.loads(line) for line in log.read_text().splitlines()]
    preview = next(e for e in traced if e["event_type"] == "tool.result")
    print("answer:", result["messages"][-1].content)
    print("\ntool result as traced:")
    print(preview["metadata"]["result_preview"])
    print("secrets the model saw:   ", leaks(str(tool_msg.content)))
    print("secrets in the log file: ", leaks(log.read_text()))

The first agent uses defaults: observability on, with redact_sensitive=True, and no guardrails.

Pythonstep1_default.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""Where do secrets from a tool result end up, with default settings?"""
import asyncio
from pathlib import Path

from promptise import build_agent
from promptise.observability_config import ObservabilityConfig, TransporterType

from common import run_and_report
from leaky_tool import deploy_status

LOG = Path("out/trace-default.log")


async def main() -> None:
    LOG.unlink(missing_ok=True)
    agent = await build_agent(
        model="openai:gpt-5-mini",
        extra_tools=[deploy_status],
        observe=ObservabilityConfig(  # redact_sensitive=True is the default
            transporters=[TransporterType.STRUCTURED_LOG],
            log_file=str(LOG),
        ),
    )
    await run_and_report(agent, LOG)


asyncio.run(main())
Output
answer: healthy 3/3

tool result as traced:
deploy: api-prod is healthy (3/3 pods), last release v2.14.1
debug env:
  OPENAI_API_KEY=[API_KEY]
  AWS_ACCESS_KEY_ID=[AWS_KEY]
  GH_TOKEN=gho_FAKEaaaabbbbccccddddeeeeffffgggg1234
  DATABASE_URL=postgres://[REDACTED]@db.internal:5432/app
request: Authorization: Bearer [REDACTED]

secrets the model saw:    ['openai key', 'aws key', 'github cli token', 'db password', 'bearer token']
secrets in the log file:  ['github cli token']

Two results matter here:

  • The model saw all five secrets. They went to the provider and sit in the returned messages, which a conversation store would save.

  • The trace caught four of five. The OpenAI key, AWS key, database password and bearer token were replaced. The GitHub CLI token (gho_...) was written to the log file in plain text, because the trace sanitizer only knows the classic ghp_ format. That is the token type in LiteLLM's October report.


[08]

Step 3: Redact tool results with a guardrail

Layer 1 scans each tool result before the model reads it. Use PromptiseSecurityScanner with only CredentialDetector: it is pure regex, so nothing is downloaded and it adds little latency. scan_tool_results=True makes build_agent wrap every tool.

Pythonstep2_guardrails.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
"""Redact credentials from tool results before the model, traces or logs see them."""
import asyncio
import sys
from pathlib import Path

from promptise import build_agent
from promptise.guardrails import CredentialDetector, CustomRule, PromptiseSecurityScanner
from promptise.observability_config import ObservabilityConfig, TransporterType

from common import run_and_report
from leaky_tool import deploy_status

LOG = Path("out/trace-guarded.log")

# The value after "Authorization: Bearer" (opaque tokens, not only JWTs).
BEARER_VALUE = CustomRule(
    name="bearer_value",
    pattern=r"(?i)\bbearer\s+[A-Za-z0-9\-._~+/]{16,}=*",
    description="Bearer token value",
)


async def main() -> None:
    LOG.unlink(missing_ok=True)
    guard = PromptiseSecurityScanner(
        detectors=[CredentialDetector()],  # 96 regex patterns, no model download
        custom_rules=[] if "--no-custom" in sys.argv else [BEARER_VALUE],
        scan_tool_results=True,  # scan every tool result before the model reads it
    )
    agent = await build_agent(
        model="openai:gpt-5-mini",
        extra_tools=[deploy_status],
        guardrails=guard,
        observe=ObservabilityConfig(
            transporters=[TransporterType.STRUCTURED_LOG],
            log_file=str(LOG),
        ),
    )
    await run_and_report(agent, LOG)


asyncio.run(main())

First, without the custom rule:

Output
answer: healthy 3/3

tool result as traced:
deploy: api-prod is healthy (3/3 pods), last release v2.14.1
debug env:
  OPENAI_API_KEY=[OPENAI_API_KEY]
  AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY]
  GH_TOKEN=[GITHUB_OAUTH]
  DATABASE_URL=[POSTGRES_CONNECTION]
request: [BEARER_TOKEN] 7f3cFAKE9a1e5b2d4f6a8c0e1b3d5f7a

secrets the model saw:    ['bearer token']
secrets in the log file:  ['bearer token']

The detector caught the OpenAI key, the AWS key, the GitHub token and the whole database URL, and the model never saw them. But the bearer token survived. Its pattern matches Authorization: Bearer and stops at the space, so it replaced the header name and left the token. Worse, the trace sanitizer, which caught this token in step 2, no longer sees the word Bearer after the first redaction, so the token reached the log file too. JWT bearer tokens are caught by a separate JWT pattern; opaque tokens like this one are not.

This is why you test with your own formats. The fix is one CustomRule that matches the value after Bearer:

Output
answer: healthy 3

tool result as traced:
deploy: api-prod is healthy (3/3 pods), last release v2.14.1
debug env:
  OPENAI_API_KEY=[OPENAI_API_KEY]
  AWS_ACCESS_KEY_ID=[AWS_ACCESS_KEY]
  GH_TOKEN=[GITHUB_OAUTH]
  DATABASE_URL=[POSTGRES_CONNECTION]
request: [BEARER_VALUE]

secrets the model saw:    []
secrets in the log file:  []

Now nothing reaches the model, the returned messages or the log. Because the guardrail rewrites the tool result itself, the trace records the redacted text, and so would a conversation store or memory.

How the guardrail behaves:

  • Redaction keeps the result useful. The model still sees "healthy (3/3 pods)" and answers correctly.

  • Overlapping matches are merged. The header and the custom rule matched the same span, so it was replaced once.

  • A `BLOCK` action withholds the whole result. Set CredentialDetector(action=Action.BLOCK) (Action is in promptise.guardrails) for tools that should never return credentials. The model then gets a short notice instead of the result.


[09]

Step 4: Close the gaps in traces and your own log lines

Layer 2 covers what layer 1 does not see: tool arguments, error messages, prompts recorded at the FULL level, and your application's own log lines. One scrub() function serves both. It starts from Foundry's redact_sensitive and adds the formats it lacks.

Pythonredaction.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
"""One redaction function for traces and log lines.

Starts from Promptise's built-in trace sanitizer and adds token formats it
does not cover. Extend EXTRA with the formats your own tools can return.
"""
import logging
import re
from typing import Any

from promptise.observability import redact_sensitive

EXTRA = [
    # GitHub OAuth, user-to-server, server-to-server and refresh tokens
    (re.compile(r"\bgh[oprsu]_[A-Za-z0-9]{36,}\b"), "[GITHUB_TOKEN]"),
    (re.compile(r"\bgithub_pat_[A-Za-z0-9_]{50,}\b"), "[GITHUB_TOKEN]"),
    # Slack bot and user tokens
    (re.compile(r"\bxox[abprs]-[A-Za-z0-9-]{10,}"), "[SLACK_TOKEN]"),
]


def scrub(text: str) -> str:
    text = redact_sensitive({"t": text})["t"]  # sk-, AKIA, ghp_, Bearer, URL passwords, PII
    for pattern, label in EXTRA:
        text = pattern.sub(label, text)
    return text


def _walk(value: Any) -> Any:
    if isinstance(value, str):
        return scrub(value)
    if isinstance(value, dict):
        return {k: v if k in {"run_id", "entry_id", "parent_id"} else _walk(v) for k, v in value.items()}
    if isinstance(value, (list, tuple)):
        return type(value)(_walk(v) for v in value)
    return value


def sanitize(metadata: dict[str, Any]) -> dict[str, Any]:
    """Sanitizer for ObservabilityCollector: every recorded event passes through it."""
    return _walk(metadata)


class RedactingFilter(logging.Filter):
    """Scrub every record that reaches a handler, including library loggers."""

    def filter(self, record: logging.LogRecord) -> bool:
        record.msg = scrub(record.getMessage())
        record.args = None
        return True

To use it for traces, pass your own ObservabilityCollector with sanitizer=sanitize; it replaces the default sanitizer. For log lines, add RedactingFilter to the handler. A filter on a handler sees every record that reaches it, including records from library loggers, while a filter on one logger only sees that logger's own records.

Pythonstep3_logs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
"""Close the gaps: a stricter trace sanitizer and a redacting log handler."""
import asyncio
import logging
from pathlib import Path

from promptise import build_agent
from promptise.observability import ObservabilityCollector
from promptise.observability_config import ObservabilityConfig, TransporterType

from common import run_and_report
from leaky_tool import FAKE_OUTPUT, deploy_status
from redaction import RedactingFilter, sanitize

LOG = Path("out/trace-sanitized.log")

# Your own log lines: put the filter on the handler, not on one logger.
handler = logging.StreamHandler()
handler.addFilter(RedactingFilter())
logging.basicConfig(level=logging.INFO, handlers=[handler], format="%(name)s: %(message)s")
log = logging.getLogger("deploy-bot")


async def main() -> None:
    LOG.unlink(missing_ok=True)
    log.info("raw tool output: %s", FAKE_OUTPUT.splitlines()[4])  # the GH_TOKEN line

    agent = await build_agent(
        model="openai:gpt-5-mini",
        extra_tools=[deploy_status],
        observer=ObservabilityCollector(sanitizer=sanitize),  # replaces the default
        observe=ObservabilityConfig(
            transporters=[TransporterType.STRUCTURED_LOG],
            log_file=str(LOG),
        ),
    )
    await run_and_report(agent, LOG)


asyncio.run(main())

This run turns guardrails off to show layer 2 alone:

Output
deploy-bot: raw tool output:   GH_TOKEN=[GITHUB_TOKEN]
httpx: HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
httpx: HTTP Request: POST https://api.openai.com/v1/chat/completions "HTTP/1.1 200 OK"
answer: healthy 3/3

tool result as traced:
deploy: api-prod is healthy (3/3 pods), last release v2.14.1
debug env:
  OPENAI_API_KEY=[API_KEY]
  AWS_ACCESS_KEY_ID=[AWS_KEY]
  GH_TOKEN=[GITHUB_TOKEN]
  DATABASE_URL=postgres://[REDACTED]@db.internal:5432/app
request: Authorization: Bearer [REDACTED]

secrets the model saw:    ['openai key', 'aws key', 'github cli token', 'db password', 'bearer token']
secrets in the log file:  []

The log line and the trace are clean, and the GitHub token is now caught. The model still saw every secret, because layer 2 runs only when text is written down. Use both layers together in production.


[10]

Running it in production

Use both layers. Layer 1 keeps secrets away from the provider and stored history. Layer 2 catches what reaches records through other paths.

Keep one pattern list. Put your extra patterns in one module and use it in the guardrail (CustomRule), the trace sanitizer and the log filter. When a new token format shows up, you add it once.

Test redaction in CI. Keep a fixture of fake secrets in every format your tools can return, run it through scrub() and through the guardrail, and fail the build if any value survives. Formats change, and providers add new key prefixes.

Limit what you record. For traces sent to a hosted backend, set record_tool_io=False. Events then keep the tool name, latency, status and result length. Set retention on log storage; a secret in a log kept for 30 days is less of a problem than one kept for years.

Restrict who can read traces. Treat trace storage like production data. LiteLLM's March report lists least-privilege access to spend logs and telemetry backends as a fix.

Other tracing backends work the same way. If you export to a hosted tracer, use its client-side hook as well: LangSmith has create_anonymizer and create_secret_anonymizer passed as Client(anonymizer=...) (docs), and Langfuse takes a mask function on the client (docs). For a comparison of tracing tools, see best AI agent observability tools.

When a secret does leak, rotate it. Redaction does not undo a leak. If a real key reached a log, a trace or a provider, revoke it and follow leaked LLM API key: rotate and audit in 30 minutes.


[11]

Common problems and fixes

Symptom

Cause

Fix

Trace shows [API_KEY] but the model answer quotes the key

Only the trace sanitizer is on

Add scan_tool_results=True with a CredentialDetector

A gho_, github_pat_ or Slack token appears in trace logs

Not in the trace sanitizer's pattern list

Pass a collector with your own sanitizer, as in step 4

Authorization: Bearer is redacted but the token after it is not

The guardrail's bearer pattern stops at the scheme word

Add a CustomRule for the value after Bearer

A real-looking OpenAI key is not redacted by the guardrail

The OpenAI pattern requires the T3BlbkFJ marker that real keys contain

Test with real formats; add a rule for custom key formats

Emails and IDs show up as [EMAIL] and debugging is hard

The trace sanitizer also redacts common PII

Debug locally with redact_sensitive=False, never in shared traces

Your log filter misses lines from a library

The filter is attached to one logger

Attach it to the handler

A secret is still in last month's logs

Redaction only applies to new records

Rotate the secret, then purge or expire the old logs


[12]

Limitations

  • Regex redaction only finds known formats. Random passwords, internal tokens and secrets split across lines get through. Not recording is the only complete fix.

  • Redaction can over-match: long hex strings and IDs can look like tokens. Check that redacted traces are still useful.

  • Layer 1 covers tool results and agent output. It does not change what your own code sends to tools as arguments.

  • The behavior shown is Promptise Foundry 1.6.0. Pattern lists change between versions, so rerun your fixture tests when you upgrade.


[13]

Frequently asked questions

How do I redact API keys from LLM logs?

Scan text for known key formats and replace matches before anything is written. In Promptise Foundry, the trace sanitizer does this for every recorded event by default, and a logging filter does it for your own log lines. Test with fake keys in each format, because no pattern list covers everything.

Do LLM providers see secrets in tool results?

Yes. A tool result becomes part of the next request to the model, so any secret in it is sent to the provider and kept in the conversation history. Redact tool results before the model reads them, not only in your logs.

What is the difference between redacting traces and redacting tool results?

Redacting traces cleans the record of what happened. Redacting tool results changes what the model sees. Only the second keeps a secret away from the provider and stored conversations, and only the first catches secrets in tool arguments and errors. Use both.

Should I log prompts and responses in production?

Log metadata by default: model, token counts, latency, tool names and status. Record full text only where you need it for debugging, keep it inside your trust boundary, redact it, and set a retention period.

Does Promptise Foundry redact secrets by default?

Partly. When observability is on, every recorded event is sanitized for common key formats, bearer tokens, URL passwords and PII. Tool results are scanned before the model reads them only when you add a guardrail scanner with scan_tool_results=True.

What should I do if a key already leaked into my logs?

Revoke and rotate the key first, then check the provider's usage for activity you don't recognize, then remove or expire the logs. Redacting future logs does not make an exposed key safe.


[14]

Next steps

  • AI agent guardrails

  • AI agent observability: trace tool calls

  • AI-based PII detection and redaction tools

  • Secrets in prompts and safe patterns

  • LiteLLM alternatives: LLM gateways compared

  • AI agent audit logging

Contents

  1. 01How do you stop secrets leaking into LLM logs and traces?
  2. 02How do secrets end up in LLM logs and traces?
  3. 03What does Promptise Foundry redact by default?
  4. 04When to redact, and when not to record at all
  5. 05What you need
  6. 06Step 1: Build a tool that leaks
  7. 07Step 2: See where secrets go with default settings
  8. 08Step 3: Redact tool results with a guardrail
  9. 09Step 4: Close the gaps in traces and your own log lines
  10. 10Running it in production
  11. 11Common problems and fixes
  12. 12Limitations
  13. 13Frequently asked questions
  14. 14Next steps

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

> Guardrails and security

Related guides

All guides in this topic →
  • // 01Beginner · 8 minLeaked LLM API Key? Rotate and Audit in 30 MinutesLeaked an OpenAI, Anthropic or other LLM API key? A 30-minute checklist: revoke and rotate, check usage and spend, find the source, and stop the next leak.
  • // 02Intermediate · 18 minAI Agent Prompt Injection: How to Defend Against ItHow indirect prompt injection reaches AI agents through tool results, and how to defend: untrusted-data prompts, scanners, approval gates, least privilege.
  • // 03Intermediate · 18 minHidden Text Prompt Injection: Sanitize Pages for AI AgentsStop hidden text prompt injection: strip CSS-hidden text, comments and invisible characters from web pages, scan what is left, and limit agent tools.
  • // 04Intermediate · 18 minAI Agent Code Execution Sandbox: Run LLM Code SafelyLet an AI agent write and run Python in a Docker sandbox: analyse a CSV, try real escape attempts, set limits, and learn where the sandbox stops.
  • // 05Intermediate · 20 minAutoGen Docker Task Isolation: Secure the Code ExecutorSet up AutoGen Docker task isolation: configure DockerCommandLineCodeExecutor, run one container per task, cut the network, set limits and fix errors.
  • // 06Intermediate · 18 minRun Coding Agents in Dev Containers SafelyHow to run coding agents like Claude Code and Codex in a dev container safely: an egress allowlist, no sudo, read-only Git hooks, and checks that prove it.

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths