Idea In 30 Seconds
Write Access by Default is an anti-pattern where an agent gets write tools by default, without a policy gate and checks before executing the action.
As a result, a model error or wrong route becomes not just a wrong answer, but a real external state change.
Simple rule: write should not be "default". It should go through a separate controlled path with explicit checks of rights, context, and execution conditions.
Anti-Pattern Example
The team builds a support agent that reads order status and can close a ticket or send an email when needed.
The agent gets write tools immediately, with no dedicated check stage before action.
decision = agent.decide_next_action(user_message)
# route is wrong, but write is still available
result = run_tool(decision.tool, decision.args)
return result
In this setup, baseline protection is missing:
# no deny-by-default for write-tools
# no approval_required for risky actions
# no idempotency_key for retries
# no strict tenant/env scope
For this case, you need a policy gate before any write step:
if decision.tool in WRITE_TOOLS and not is_write_allowed(ctx, decision):
return stop("approval_required")
If conditions are not met, the run must not proceed to external write action.
In this case, Write Access by Default adds:
- risk of unwanted changes in external systems
- duplicated write operations during retries
- larger blast radius in multi-tenant environments
Why It Happens And What Goes Wrong
This anti-pattern often appears when a team wants the agent to be "maximally autonomous" and opens write access before building control boundaries.
Typical causes:
- demo-first approach: grant all tools first, add limits later
- no explicit separation of
readandwriteroutes - access rules are described in prompt, not enforced in gateway
- no mandatory
idempotency_keyfor write operations
As a result, teams face:
- unsafe side effects (state changes) - the agent can write where it should only read
- repeated actions - retry or loop repeats the same write operation
- high blast radius - without strict scope, failure hits the wrong tenant/env
- hard incident analysis - difficult to prove why write was allowed
- loss of predictability - the team cannot control when the system moves to write
Unlike Blind Tool Trust, the core issue here is not payload validation, but default-open write access.
Typical production signals that write control is weak:
- write tools are called even in scenarios that should be read-only
- logs contain many write calls with the same
args_hashor withoutidempotency_key approval_requiredalmost never appears even with high write share- blocked write attempts are rare although the system proposes risky actions regularly
- audit logs do not show which policy rule allowed a write
- the team cannot clearly explain why a specific write was allowed in this run
- failures are discovered after the external action, not at policy gate
Important: every write call changes external state and often has no easy rollback. Without deny-by-default and a controlled write path, one bad inference becomes a production incident.
Correct Approach
Start with a read-first model: read tools are route-available, while write steps pass dedicated checks through a policy gate.
Practical framework:
- enforce deny-by-default for all write tools
- split routes into
read_onlyandwrite_candidate - require approval for risky write actions
- derive
tenant_idandenvonly from authenticated context, never from model output - add
idempotency_keyto every write operation - log
stop_reasonand policy-gate decisions for each write step
WRITE_TOOLS = {"ticket.close", "refund.create", "email.send"}
def execute_action(user_message: str, ctx: dict):
decision = agent.next_action(user_message)
if decision.tool in WRITE_TOOLS:
if not is_write_allowed(ctx, decision): # policy gate: role, route allowlist, tenant/env scope
return stop("approval_required")
scoped_args = enforce_scope(
decision.args,
tenant_id=ctx["tenant_id"],
env=ctx["env"],
)
scoped_args["idempotency_key"] = make_idempotency_key(ctx["run_id"], decision)
return run_tool(decision.tool, scoped_args)
return run_tool(decision.tool, decision.args) # read-only tool from allowed set
In this setup, the write step is controlled: the system either executes safely or transparently stops the run.
Quick Test
If the answer to these questions is "yes", you have Write Access by Default anti-pattern risk:
- Can a write tool be called without explicit policy/approval checks?
- Do retries sometimes repeat the same write action without
idempotency_key? - Can the team not quickly explain why a specific write was allowed?
How It Differs From Other Anti-Patterns
Blind Tool Trust vs Write Access by Default
| Blind Tool Trust | Write Access by Default |
|---|---|
| Main problem: tool output is accepted without validation. | Main problem: write access is open by default. |
| When it appears: when parse/schema/invariant checks are missing before decisions. | When it appears: when write does not pass through deny-by-default and approval gate. |
In short: Blind Tool Trust is about data quality before action, while Write Access by Default is about permissions for the action itself.
Agents Without Guardrails vs Write Access by Default
| Agents Without Guardrails | Write Access by Default |
|---|---|
| Main problem: runtime boundaries and policy control are broadly missing. | Main problem: write operations specifically have no strict access contour. |
| When it appears: when the system lacks clear safety policy for execution. | When it appears: when write is allowed as a standard route instead of an exception via policy gate. |
In short: Agents Without Guardrails is a broader execution-boundary issue, while Write Access by Default is specifically about unsafe write access model.
Tool Calling for Everything vs Write Access by Default
| Tool Calling for Everything | Write Access by Default |
|---|---|
| Main problem: tools are called unnecessarily, even when avoidable. | Main problem: once a tool call happens, write can pass without sufficient control. |
When it appears: when there is no stable no_tool route for simple cases. | When it appears: when the system does not separate read and write access levels. |
In short: Tool Calling for Everything increases call count, while Write Access by Default increases failure cost of each write call.
Self-Check: Do You Have This Anti-Pattern?
Quick check for anti-pattern Write Access by Default.
Mark items for your system and check status below.
Check your system:
Progress: 0/8
β There are signs of this anti-pattern
Move simple steps into a workflow and keep the agent only for complex decisions.
FAQ
Q: Does this mean agents should never perform write actions?
A: No. Write actions are possible, but only through a controlled path: policy gate, approval where needed, scope enforcement, and idempotency.
Q: What is the difference between policy gate and approval?
A: Policy gate is deterministic rule enforcement at runtime. Approval is a separate confirmation for a specific risky action. They are different control layers.
Q: What is the minimum to implement first?
A: Start with deny-by-default for write, mandatory idempotency_key, strict tenant/env scope, and stop_reason logging for blocked write attempts.
What Next
Related anti-patterns:
- Blind Tool Trust - when the system acts on unvalidated tool output.
- Agents Without Guardrails - when execution runs without clear runtime boundaries.
- Tool Calling for Everything - when tools are called without explicit need.
What to build instead:
- Allowed Actions - how to define allowed actions via explicit rules.
- Tool Execution Layer - where to centralize policy, scope, and idempotency.
- Stop Conditions - how to safely stop a run when write is not allowed.