# How do I keep an agent risk and approval record outside the agent's editable session?

**Short answer:** Store approvals and risk decisions where the agent cannot rewrite them: a server-owned database, an append-only ledger or a checkpointer the agent's tools can't open for write. Don't keep the only copy in chat history, the agent's scratch folder, or a serialized run state the client can edit. The OpenAI Agents SDK's human-in-the-loop guide says to keep the complete RunState snapshot in application-controlled server storage for browser or mobile approval UIs, send reviewers only display details and opaque ids, and never accept replacement tool calls or serialized state from the client. LangGraph's persistence docs say InMemorySaver is lost on restart and point production at PostgresSaver or SqliteSaver. Bind each yes to a hash of the exact action, expire it, and verify that hash again right before the risky step runs.

## Checklist
1. Decide which events are the risk record: approval requests, approve/deny decisions, policy changes, denials, expiries and revocations. Write each as its own append-only event with who, what, when and which agent.
2. Put that store outside the agent's writable session: a server database the agent's tools can't UPDATE/DELETE, an external ledger, or a checkpointer/credentials vault the agent process doesn't mount read-write.
3. Never treat chat memory, AGENTS.md, a local JSON file in the workspace, or the model's context as the system of record for approvals.
4. For OpenAI Agents SDK pauses: keep result.state.toString() on the server; authenticate the reviewer from your session middleware (not the request body); atomically consume the pending request before resume so the same snapshot can't be replayed.
5. For LangGraph: use a durable checkpointer (PostgresSaver or SqliteSaver), not InMemorySaver, and put interrupt()/resume on a thread_id your payment or tool runner owns.
6. Show the reviewer the real arguments (tool name, amount, target), bind the decision to a hash of them, make the token single-use with a short expiry, and re-check the hash immediately before execution.
7. Give the agent only a receipt id or opaque approval id, not the private decision token and not a mutable "approved=true" flag in its session.
8. Log refusals and timeouts as well as approvals, and keep personal data to a stable user id where you can (OWASP logging guidance).

## Why the agent's session is the wrong place
- Anything in the prompt, tool results or a file the agent can edit can be changed by the next turn, a prompt injection or a hallucinated "user already approved". A risk record has to survive that.
- OpenAI's Agents SDK docs warn that RunState.fromString() restores execution state and does not authenticate the snapshot or the person submitting it. Schema checks and tool-call fingerprints are not authorization. Keep snapshots in trusted storage and verify integrity, ownership and replay protection before deserializing.
- Their server-owned approval pattern: store ownerId + snapshot + decisionIds server-side; return only requestId and display prompts; on decide(), check the authenticated user owns the request, validate boolean decisions against stored ids, delete/consume the pending entry, then approve/reject and resume.
- LangGraph persists short-term thread state with checkpointers. InMemorySaver lives in RAM and is gone on restart; use PostgresSaver (or SqliteSaver for local work) when an approval must still be there after a deploy or crash.

## What to record, and how to use it at runtime
- For each risky action: grant or approval id, approver identity from your IdP, agent id, purpose, allowed actions and targets, limits, start/expiry, decision, and a hash of the exact arguments shown.
- Carry only the public receipt or approval id into the agent run. The tool runner loads the record from the external store, checks expiry and action hash, then runs or refuses. If the arguments changed, ask again.
- MCP's tools spec says there SHOULD always be a human in the loop able to deny tool invocations; leave confirmation prompts on for servers that can pay, delete or send. Don't satisfy that with a checkbox the model can flip in its own notes.
- Separate standing policy (who may approve what, spend caps) from one-shot approvals. Policy changes get their own signed events with an author; one-shot yeses expire and are single-use.

## What can go wrong?
- The only approval flag lives in the agent's session JSON, and a later turn overwrites it to true.
- A browser client is allowed to upload a full serialized RunState; an attacker edits pending tool arguments and re-submits.
- Approvals are sticky forever (alwaysApprove) with no expiry, so an old yes covers a new, larger purchase.
- The durable checkpointer and the agent's workspace share a volume the agent can write, so it can edit checkpoint rows.

## Ask Rightshold (free, no key)
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code (one-click setup: https://www.rightshold.com/start?ref=seo), then ask: "Before my agent runs this risky tool call, ask me to approve the exact action (tool name and arguments), store the decision outside the agent's session, and wait for my yes or no."

```json
{
  "tool": "request_human_approval",
  "arguments": {
    "summary": "Allow research-agent to call transfer_funds for 25.00 USD to vendor-settlement.example.com",
    "action": {
      "type": "tool_call",
      "tool": "transfer_funds",
      "amount_minor": 2500,
      "currency": "USD",
      "merchant_domain": "vendor-settlement.example.com"
    },
    "expires_in_minutes": 30
  }
}
```
- Needs your API key. The action is hashed; you decide once with decide_human_approval (approve or deny). The decision is a signed, timestamped receipt on Rightshold's ledger, and the one-time token is shown once — keep it off the agent's writable session.
- verify_human_approval (free) checks a decision receipt and that the action matches what was approved, so a changed amount or target fails on the same yes.
- With an account key, set_spend_policy and authorize_spend add per-purchase and daily caps as a second control that also lives outside the agent process.
- How to audit which human authorized an AI agent action covers grant trails and RFC 8693/9396 vocabulary.

_Rightshold records approval requests and signed decisions for agents that use it. It doesn't host your RunState blobs, run your identity provider, or stop an agent that never asks. Keep framework snapshots and checkpointers in storage the agent cannot edit. Not legal advice._

## Sources
- [OpenAI Agents SDK (JS): Human-in-the-loop (keep approval state on the server)](https://openai.github.io/openai-agents-js/guides/human-in-the-loop/)
- [LangGraph: Persistence (checkpointers vs stores; InMemorySaver vs PostgresSaver)](https://docs.langchain.com/oss/python/langgraph/persistence)
- [Model Context Protocol specification 2026-07-28: Tools (human in the loop)](https://modelcontextprotocol.io/specification/2026-07-28/server/tools)
- [OWASP Logging Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)
- [Human approvals on Rightshold](https://www.rightshold.com/human-approvals)

## Related
- [How to audit which human authorized an AI agent action](https://www.rightshold.com/answers/how-to-audit-which-human-authorized-an-ai-agent-action)
- [How to require human approval before an AI agent spends money](https://www.rightshold.com/answers/how-to-require-human-approval-before-an-ai-agent-spends-money)
- [How to show a human the actual change before approving an AI agent action](https://www.rightshold.com/answers/how-to-show-a-human-the-actual-change-before-approving-an-ai-agent-action)
- [How to give an AI agent a scoped, expiring credential instead of a long-lived API key](https://www.rightshold.com/answers/how-to-give-an-ai-agent-a-scoped-expiring-credential-instead-of-a-long-lived-api-key)

_Not legal advice. Updated 2026-11-06. JSON: https://www.rightshold.com/answers/how-to-keep-an-agent-risk-approval-record-outside-the-agents-editable-session.json_