8 min read

Agent tool errors that help without exposing secrets

Design agent tool errors that guide retries and approvals without exposing tokens, headers, raw connection failures, or sensitive diagnostics.

Agent tool errors that help without exposing secrets

Agent tool errors need enough structure for an agent to recover, but they must not turn every failed request into a credential export. That boundary is easy to describe and routinely violated in practice: a wrapper catches an exception, returns the library message, and quietly hands the model a URL, an Authorization header, a certificate subject, an SSH target, or a token fragment.

I have seen the damage start with a well-meant debugging field. Someone adds request_headers because an agent kept getting 401 responses. The agent copies the error into its working notes. Those notes get pasted into a pull request, sent to a support system, or retained in an evaluation trace. The original failure has passed. The secret has not.

The right design gives the agent a bounded explanation of what happened, what it may safely do next, and a correlation handle for a human operator. It keeps raw evidence behind the action boundary. This is not a choice between useful errors and secret-safe errors. It is an interface design job.

An error response is part of the security boundary

An agent treats tool output as working memory. It may quote that output to a user, place it in a file, send it to another tool, or use it to choose a retry. Any field returned from an action gateway must therefore be treated as disclosure to an untrusted caller, even when the caller is an agent launched by a developer who owns the machine.

A tool call has two audiences. The agent needs operational facts: did the action run, can it retry, does it need consent, and what input should it change? The operator needs forensic facts: which credential was selected, what exact route was called, which DNS resolver failed, and what the remote peer said. Do not satisfy the second audience by dumping its evidence to the first.

The distinction matters most when an agent can invoke tools repeatedly. A human seeing a verbose error once may notice a bearer token in it. An agent may preserve it across dozens of turns, then include it in generated code or a test fixture. The model does not need malicious intent for that to happen.

Build the interface around an explicit rule: the caller receives only fields that remain safe if copied into a public issue. If a field fails that test, store it in protected diagnostics or omit it.

Give agents decisions, not exception strings

A useful response tells the agent what kind of decision it faces. Raw exceptions do neither reliably nor safely. Their wording changes across operating system releases and client library versions, and they often mix together a technical cause with material that must stay private.

Use a small, documented vocabulary of codes. Each code should map to a specific caller behavior. Do not make codes so detailed that you end up recreating every possible upstream failure.

A practical response shape looks like this:

{
  "ok": false,
  "code": "AUTH_FAILED",
  "message": "The remote service rejected the stored credential.",
  "action": "stop_and_report",
  "retryable": false,
  "request_id": "act_7f3c2a91",
  "http_status": 401
}

The agent can stop, tell the user that authentication needs attention, and include the request ID. It does not need the bearer token, the authorization scheme, the account email, or a copied response body to make that decision.

action is more useful than a vague boolean alone. A boolean says whether time might fix the issue. An action tells the agent what it should do. Keep the allowed values tight:

  • retry_after_delay for temporary, safely repeatable operations.
  • repair_input for a request the agent can correct without new authority.
  • request_approval when a human must permit the action.
  • stop_and_report for failures that need operator work.
  • inspect_outcome when a write may have reached the remote service.

The last one deserves special treatment. A timeout after sending a write is not the same as a timeout before sending it. If you flatten both cases into NETWORK_ERROR, an agent will retry a possibly completed action. That is how duplicate tickets, deployments, records, and destructive commands happen.

Expose http_status only when it is meaningful and safe. It is often useful context for HTTP calls, but do not pretend that it gives the complete answer. A 403 can mean an upstream authorization failure, a resource-level restriction, or a gateway decision. Your tool code must name the behavior the agent should take.

Keep a stable code taxonomy small enough to test

A code taxonomy should describe ownership and recovery, not every layer of the network stack. If your list has fifty codes after the first week, you are probably exporting implementation details under a different name.

Start with categories that let the caller take a different action:

CodeMeaningAgent behavior
INVALID_INPUTThe tool rejected supplied fields before an external action.Repair input.
VAULT_LOCKEDThe gateway cannot use any stored secret.Ask a person to unlock it.
USER_DENIEDA person declined this action.Stop. Do not rephrase and resubmit.
AUTH_FAILEDThe remote service rejected the selected credential.Stop and report.
REMOTE_FORBIDDENThe request authenticated but lacks remote permission.Stop and report.
RATE_LIMITEDThe remote service asked callers to slow down.Wait if a safe delay exists.
TEMPORARY_FAILUREA repeatable request failed temporarily.Retry within a bounded budget.
OUTCOME_UNKNOWNA write may have completed before failure.Inspect before any retry.
NETWORK_UNREACHABLEThe gateway could not reach a remote endpoint.Retry only when the action is safe to repeat.
INTERNAL_FAILUREThe gateway failed without a safe caller remedy.Stop and report the request ID.

Do not use ERROR, FAILED, or EXCEPTION as your primary contract. Those labels transfer the interpretation burden to the agent, which will infer a remedy from prose. It might infer badly.

Keep code meanings stable. You may improve the human message, add a retry_after_seconds field, or include a newly safe status value. Do not change AUTH_FAILED to mean both bad credentials and a local approval refusal. Agents and their surrounding orchestration will eventually branch on it.

RFC 9457, "Problem Details for HTTP APIs," gives a useful baseline: responses can carry a stable problem type, a title, status, detail, and instance reference. Its warning is the part teams skip. The RFC says that detail should help correct the problem and that problem details can expose sensitive information. For agent tools, make the stable type or code the contract, keep detail short, and use the instance or request ID to connect an operator to protected evidence.

Request headers and connection errors are evidence, not context

Teams often call raw headers and transport messages "context." They are evidence. Evidence belongs in an audit record with access controls, not in an agent response.

Consider a failed API request. A typical HTTP library exception may include the complete requested URL, redirect location, proxy address, response headers, and part of the response body. Any of those can carry secrets. Query parameters still carry API keys in older APIs. Location headers often contain signed download URLs. Cookies and custom authentication headers are obvious leaks. Less obvious fields such as X-Request-Id may be fine, while X-Forwarded-Host or an internal service header may reveal infrastructure that the agent never needed to know.

SSH errors need the same discipline. Do not return a command line containing a private target, a known-hosts path, an offered identity file, or the raw host-key mismatch text. A host-key mismatch has a caller-relevant meaning: the connection is blocked because the remote identity cannot be verified. Return that meaning. Preserve the fingerprint comparison, paths, and library diagnostics for an operator.

OAuth 2.0 makes this point unusually clear. RFC 6750 says a bearer token gives access to whoever possesses it, and it directs clients to protect tokens from disclosure in storage and transport. An error handler that copies a bearer token into a trace has defeated that requirement even if the original request used TLS correctly.

Sanitize before serialization, not after logs spread. A redaction filter on a general log sink is useful as a backstop, but it is not the boundary. By that point, a thrown exception may already have been attached to a tool result, a telemetry event, or a crash report.

Use an allowlist for fields crossing to the agent. A denylist eventually misses x-api-token, a signed query parameter, a vendor-specific session field, or a new library property. An allowlist starts with no disclosure and adds only fields with an identified caller use.

Separate what failed from whether an action ran

Keep credentials behind the gateway
Agents connect through sp mcp while Sallyport holds credentials in its encrypted in-app vault.

A safe error design must tell an agent whether the remote side might have acted. This is where most retry guidance becomes dangerous.

Suppose an agent sends POST /deployments and its connection times out. The gateway knows it attempted the call. It does not know whether the upstream received it, whether the upstream created a deployment, or whether the response disappeared on the way back. Returning TEMPORARY_FAILURE invites the agent to send another deployment request. Returning AUTH_FAILED is simply false. The correct state is OUTCOME_UNKNOWN.

The response should say so plainly:

{
  "ok": false,
  "code": "OUTCOME_UNKNOWN",
  "message": "The connection ended after the request started. The remote action may have completed.",
  "action": "inspect_outcome",
  "retryable": false,
  "request_id": "act_9b18d4e0",
  "operation": "create_deployment"
}

operation names a generic action class, not the full route or payload. The agent can now use a separate, read-only status call if the integration offers one. If the API supports idempotency references, the gateway can associate the safe reference with the operation and query it internally. Do not expose an idempotency value if it doubles as access material in the target system.

Read operations are not automatically safe to retry either. A read can trigger billing, refresh a remote state, or run a command with side effects behind an innocent name. The integration author must label whether an operation is repeatable. Do not ask a language model to decide that from the method name.

Set a retry budget in the gateway. A response can include a bounded delay such as retry_after_seconds: 30, but only when the upstream gave a safe value or the gateway owns the limit. Do not let an agent retry indefinitely because a message said "temporary." Repeated failures create noise, consume rate limits, and make a later investigation harder.

Human denial needs its own meaning

A person declining an approval is not a remote authentication error. It means the requested action did not execute. That distinction protects both security and usability.

If a tool maps a denied approval to AUTH_FAILED, an agent may try alternate credentials, ask to rotate a secret, or attempt a slightly changed request. None of those actions honors the person who said no. If it maps denial to a generic internal failure, the user cannot tell whether the gateway malfunctioned.

Return USER_DENIED with a short message such as "The requested action was not approved and did not run." Avoid mentioning the secret name, the selected account, or the exact target if those details are not already part of the tool's safe input contract. The caller needs to stop. A person can decide whether to initiate a new, visible request.

A locked secret store is different again. VAULT_LOCKED means the gateway refused the action before it could select or use a credential. The safe remedy is to ask a person to unlock the gateway, not to ask the agent to provide a token. That prevents a familiar failure mode where a model compensates for unavailable managed credentials by searching its context for another secret.

Sallyport uses this separation directly: while its vault gate is locked, it denies every action, and its per-session authorization can distinguish a new process that needs approval from a remote service that rejected an authenticated call. The agent receives the action result, while the credential stays in the app's encrypted vault.

Do not paper over approval fatigue by returning more diagnostic detail. If users routinely approve calls they have not inspected, fix the action grouping, scope, and process identity presented for approval. More verbose denials do not make rushed consent safer.

Build a two-record diagnostic path

Keep SSH keys off limits
Run SSH through the bundled sp-ssh helper while the agent never receives the SSH key.

One record should be safe for the agent and one should be complete enough for an authorized operator. Trying to make a single record fit both jobs produces either an opaque support experience or a secret leak.

The caller-safe record needs a code, message, action, retry guidance, request ID, and perhaps a protocol status. The protected record can contain the selected credential record identifier, normalized destination, method, timing, upstream response metadata, sanitized payload fingerprint, raw exception, and a trace of the gateway decision. Store the protected record in a place that agents cannot query through ordinary tools.

A request ID should be opaque. Generate it independently of credentials and destinations. Do not encode a hostname, username, timestamp that reveals activity patterns, or an incrementing database ID if those details matter in your environment. The ID lets a user say, "please inspect act_9b18d4e0," without handing them the underlying diagnostics.

For high-value actions, record whether the gateway reached each boundary: input validation, credential selection, user authorization, connection started, request bytes sent, response received, and result returned. That sequence gives an operator a defensible explanation of OUTCOME_UNKNOWN without showing the agent the raw request.

Tamper evidence matters for this internal path. If someone can silently edit away failed authorization attempts or rewrite the reason an action was blocked, the audit trail becomes a convenience log. Sallyport projects session and call views from an encrypted, hash-chained audit log, and sp audit verify checks the chain offline over ciphertext. That is useful when an operator needs to trust the record without granting the agent access to its contents.

A sample protected record might look like this. It is intentionally not an agent response:

{
  "request_id": "act_9b18d4e0",
  "event": "http_call_failed",
  "credential_record": "cred_42",
  "destination": "api.internal.example",
  "method": "POST",
  "path_template": "/deployments",
  "bytes_sent": true,
  "response_received": false,
  "exception_class": "ReadTimeout",
  "result_code": "OUTCOME_UNKNOWN"
}

Even here, review fields carefully. A full path can expose resource identifiers. A request body should usually become a one-way fingerprint, a schema name, or a tightly redacted representation. Operators often need to compare two attempts, not read every submitted value.

Error messages need a deliberate redaction policy

Redaction is not replacing a token with eight asterisks. That only handles the secret formats you already recognize. A proper policy classifies fields before they enter messages, logs, metrics, and tool results.

Classify direct secrets first: bearer tokens, passwords, private keys, cookies, signed URLs, authorization headers, and client certificates. Then classify sensitive context: internal DNS names, local paths, user names, repository names, resource IDs, request bodies, and headers that reveal deployment topology. The second class may be acceptable in a protected record but rarely belongs in an agent-visible error.

Do not return "credential ending in 7KQ2 was rejected." Teams add this because multiple credentials exist and operators want to know which one failed. It creates a durable identifier that can be correlated across traces. Return AUTH_FAILED to the agent. Let the operator inspect the credential record through the protected request ID.

Avoid echoing tool input by default. An agent already knows what it attempted, but the gateway cannot assume that input itself is safe to repeat. A URL may contain a signed query string. A command may include an environment assignment. A JSON payload may include a temporary credential the agent received from another system. Return a field-level validation error such as invalid_fields: ["repository"], not a copied invalid value.

Test redaction with hostile fixtures. Include secrets in unusual casing, duplicated headers, URL userinfo, percent-encoded query values, nested JSON, exception causes, and SSH command arguments. Then assert that no fixture secret appears in the serialized tool response, general logs, metrics labels, or crash payloads. A test that only checks the happy path proves nothing about failure handling.

Treat remote messages as untrusted input

Leave evidence agents cannot alter
Activity and Sessions views come from one encrypted, hash-chained audit log.

A remote API can return a helpful error body, an HTML login page, or a string designed to influence whoever reads it. The gateway must not pass that content directly into an agent's context.

This is partly a secret issue. Servers sometimes reflect request values in error pages. An invalid Authorization header, cookie, query parameter, or JSON field can come back in an upstream diagnostic response. Passing it through turns a remote echo into a credential leak.

It is also an instruction-integrity issue. If an upstream error says "Run this command to repair your credentials," the agent may treat it as operational guidance. The remote service does not get to write the gateway's recovery policy.

Map known safe fields from an upstream protocol response. For example, a numeric HTTP status and a documented rate-limit delay can be useful. Treat free-text bodies as protected evidence unless you have a format-specific parser and a clear allowlist. If an integration needs a human-readable remote reason, normalize it into gateway-owned language such as "The service rejected the requested resource" rather than copying its prose.

RFC 9110 defines HTTP status semantics, but its statuses do not grant permission to disclose an origin's response body. Keep that separation clear. Protocol information can aid recovery; arbitrary upstream text is not a safe diagnostic contract.

Put the contract under failure-focused tests

Most teams test that a valid request returns useful data. Test that every invalid and interrupted request returns only the data the agent should see.

Make the error schema a versioned contract. Validate it in tests and reject unknown fields at the serialization boundary during development. An allowlist is easier to audit when the object has a strict shape.

Use a fixture secret that is ugly enough to catch accidental transformations, then force failures at each stage. Your test matrix should cover validation before credentials, a locked vault, declined consent, remote 401 and 403 responses, rate limiting, DNS failure, TLS validation failure, timeout before bytes leave, timeout after bytes leave, malformed upstream JSON, and an exception thrown by the gateway itself.

For each case, assert four things:

  • The tool response contains the expected code and permitted action.
  • The fixture secret does not occur in any caller-visible serialized field.
  • The response does not contain raw headers, raw upstream body text, or local connection details.
  • The protected diagnostic record carries the request ID and enough state for an operator to investigate.

Do not settle for regex checks alone. Search for the exact fixture secret, its URL-encoded form, base64 form when relevant, and common prefixes or suffixes. Then manually inspect a captured error response whenever you update an HTTP, SSH, telemetry, or crash-reporting dependency. Libraries change exception formatting without asking your permission.

The hard part is resisting the urge to make the agent's error resemble the operator's debug console. Keep the caller contract small, stable, and action-oriented. Keep the evidence protected and correlated. When the next outage arrives, that separation will give the agent enough to behave safely and give the human enough to fix the actual fault.

FAQ

What should an AI agent tool error include?

Return a stable machine-readable code, a short human message, the action that failed, and a safe next action. Keep request headers, credentials, full URLs with query strings, and raw transport diagnostics on the trusted side of the boundary.

Are HTTP status codes enough for agent tools?

Usually, no. An HTTP status describes the result at the protocol boundary, while an agent needs to know whether it should retry, ask for approval, repair its input, or stop. Preserve the status as safe context, then add a tool-specific code with that operational meaning.

Can an error include the first few characters of a token for debugging?

Do not return it to the agent. The gateway can log the token fingerprint or credential record internally and return a generic AUTH_FAILED response with a request ID. A token prefix is still credential material and often ends up in transcripts, issue trackers, and shell history.

Which failures may an agent retry automatically?

A retry is reasonable for an explicit temporary failure such as a timeout before any response, a connection reset, or an upstream 503 when the operation is idempotent. Do not retry authorization failures, invalid input, denied approvals, or ambiguous write failures unless the tool can establish the remote outcome first.

How should a tool handle a timeout during a write request?

Treat an unknown write outcome as its own error class. The agent should inspect a safe status endpoint, use an idempotency reference if the API supports one, or ask a person before repeating the action. Blindly replaying a payment, deployment, delete, or ticket creation is worse than a slow failure.

Should error messages stay stable across tool versions?

Keep user-facing error messages stable and test them. You may add fields over time, but changing code meanings or turning terse messages into diagnostic dumps breaks agent behavior and can expose information that old clients log without review.

Is it safe to give an agent a request ID?

A request ID is safe when it is randomly generated or otherwise non-secret and reveals no customer data, host name, credential identity, or filesystem path. It should let an operator find the full internal record without giving the agent a handle to retrieve protected diagnostics.

Can an agent read logs to diagnose a failed tool call?

Only if the source is designed for agent visibility and its fields have been reviewed. Many application logs contain authorization headers, signed URLs, cookies, SQL values, internal host names, and stack traces. A separate sanitized activity record is safer than filtering a general debug log after the fact.

What network diagnostics are unsafe to return to an agent?

Never send raw TCP, TLS, DNS, or SSH library errors directly to an agent. Map them to a bounded code such as NETWORK_UNREACHABLE, TLS_VALIDATION_FAILED, or SSH_HOST_UNVERIFIED, then retain the raw error only in protected diagnostics.

How should an agent tool report a denied approval?

Approval denial should be distinct from authentication failure and policy refusal. Return a code such as USER_DENIED, say that the requested action did not run, and avoid describing the stored credential or remote account that would have been used.

Sallyport

Sallyport runs API calls and SSH commands for your AI agent. The keys stay in a local vault on your Mac; you approve each run and every action lands in a sealed journal.

© 2026 Sallyport · Open source under Apache-2.0 · Oleg Sotnikov