A permission matrix for AI agents that work with digital assets
A permission matrix states, action by action, what an AI agent may prepare, what people approve and which channel executes it.
This article is educational and does not replace a formal smart contract audit, legal advice, financial advice, or protocol-specific security review.
An AI agent that works with digital assets needs more than a list of tools. It needs a permission matrix that states, action by action, what the agent may read and prepare, what people approve and which authorized channel executes.

The useful unit is the capability, not the tool. Signing, accessing a key, enabling a module, changing the fallback handler or approving a DELEGATECALL operation each opens authority over the account, so each needs its own row, its own approval and its own accountable person.
This article turns that idea into a matrix for a small team running a Safe: the rows and resources, what a custody provider's policy engine covers and what stays with the team, and the negative cases to run before production.
For the general principles of agent permissions in production, see the article below. This matrix focuses on keys, signing, modules, guards and the paths that bypass them.
Start from the capabilities that open authority
A tool name says little about risk. A tool called propose may only draft a payload, or it may hold a key that signs it. The matrix should therefore list capabilities, and only then map tools to them.
In a Safe smart account, these capabilities change who can move funds or how the account behaves:
- Signing or submitting a transaction, or holding the key that does it.
- Adding or removing owners and changing the threshold.
- Enabling or disabling a module. According to the Safe documentation, modules can execute arbitrary transactions, and a malicious module can take over a Safe.
- Setting a transaction guard or, from Safe 1.5.0, a module guard. According to the documentation, a broken guard can cause a denial of service for the Safe.
- Changing the fallback handler. The documentation warns that it enables the execution of arbitrary external functions within the account.
- Approving a transaction whose operation field is DELEGATECALL. The Safe supports CALL and DELEGATECALL, and with DELEGATECALL the target code runs in the context of the Safe, with its storage and balance.
- Upgrading, pausing or changing parameters of the protocol's own contracts.
One version detail changes the matrix. From Safe 1.3.0, which introduced guards, up to 1.4.1, the transaction guard checks transactions executed with execTransaction and does not cover transactions executed by modules. Safe 1.5.0 adds a separate module guard for that path, but it only applies once someone sets it: upgrading to 1.5.0, or setting only a transaction guard, leaves modules unchecked. We checked both on local forks: on a Safe 1.4.1 with a guard that rejects every call, the transaction signed by the owner reverted and the module's transaction went through; on a Safe 1.5.0, the module's transaction only reverted once the module guard was set.
- Safe: Smart account concepts
- Safe: Modules
- Safe: Guards
- Safe: Fallback handler
- Solidity: Delegatecall and libraries
The matrix, row by row
In short: the agent reads, decodes, simulates and drafts; people approve the exact payload or the specific change; an authorized channel outside the agent signs and executes. Each row below has three parts: what the agent may do, who approves and what executes.
- Read balances and configuration. Agent: typed reads limited to the listed chains and Safes. Approval: a read policy agreed in advance. Execution: a read-only tool.
- Decode and simulate. Agent: prepares evidence, pinned to a block. Approval: a person reviews coverage and results. Execution: an authorized simulator.
- Prepare a transfer. Agent: an unsigned draft. Approval: the signers review the exact payload. Execution: an authorized channel outside the agent.
- Sign or submit. Agent: outside its scope. Approval: the human signing procedure. Execution: the wallet or authorized channel.
- Rotate keys, change owners or change the threshold. Agent: a documented proposal. Approval: the person accountable for the change and the signers. Execution: the administration channel.
- Change modules, guards, the fallback handler or upgrades, or sign any DELEGATECALL. Agent: analysis and a draft. Approval: a review specific to that change. Execution: the administration channel.
- Pause a contract or change parameters. Agent: diagnosis and a proposal. Approval: the person named in the procedure. Execution: a restricted external tool.
Two rows look similar and are not. Preparing a transfer and approving a DELEGATECALL can both end in an execTransaction call, but the second can change the account itself. That is why the matrix sends it to the same review as a module or guard change.
Name the resources behind each permission
A row is only enforceable when its resources are named. For each permission, the team writes down the chains, the Safes, the token contracts, the approved recipients, the functions the agent may draft and the limits that apply.
The same goes for exceptions. If a flow needs DELEGATECALL, for example to batch calls, the matrix should name the reviewed target contract and its version, and every other DELEGATECALL target stays outside the agent's drafts. If an integration needs a module, the matrix names the module address, the person responsible for it and the date it should be removed.
What a custody provider's policy engine covers
Some teams connect the agent to a key held by a custody or wallet infrastructure provider with its own policy engine, such as Turnkey or Fireblocks. Those engines enforce rules on the requests that reach them. The matrix has to record which of its rows each engine enforces and which ones depend on other components.
According to the Turnkey documentation, every policy in an organization is evaluated on each request: any matching deny wins, otherwise a matching allow, and anything else is denied implicitly. Ethereum rules can read parsed transaction fields such as the recipient, value and calldata; function names and arguments are only available when the contract interface has been uploaded; typed data can also be evaluated. The same documentation states that actions approved by the root quorum do not go through the policy engine and cannot be limited by any policy.
According to the Fireblocks documentation, its Policy Engine applies policies to all transactions created from the Fireblocks Console or API. Rules are applied on a first-match basis from top to bottom, so their order matters. Contract call rules can filter by method. Only the workspace Owner or Admin-level users manage policies, and an approval group has to approve proposed changes.
A Safe adds authority paths that are defined in the contract, not in the provider's engine:
- A key governed by one engine may be one owner among several. The other owners' keys sign elsewhere, under other rules.
- An enabled module executes through execTransactionFromModule, and on that path the Safe does not check owner signatures; any check is the module's own. A provider's engine sees that execution only if it governs the key that calls or authorizes the module, and then it sees a request to the module, not a Safe transaction signed by the owners, so rules written for those transactions do not apply to it.
- Guards and the fallback handler are configured in the Safe itself. The transaction guard, when one is set, checks every execTransaction, whichever key signed it, but not what a module executes; on Safe 1.5.0 that path needs its own module guard. The fallback handler answers calls to functions the Safe does not implement. An enabled module can also change all of them without owner signatures, because they are calls the Safe makes to itself.
- A DELEGATECALL travels as the operation field, both in the SafeTx the owners sign, usually as EIP-712 typed data, and in the execTransaction call that submits it. A rule only stops it if it reads that field in the format the engine receives; if the key signs only the SafeTx hash, the engine has no field to read.
The provider's engine is one enforcement point in the matrix, not the matrix. For a Safe owner, the recipient an engine reads is usually the Safe itself; the real destination, value and gas refund fields sit inside the calldata or the typed data, and a rule only checks them if it decodes them. The team still decides which rules to write, who may change them, which paths bypass them, such as a root quorum, a module or another owner, and how to test those paths with negative cases.
- Turnkey: Policy overview
- Turnkey: Policy language
- Turnkey: Root quorum
- Fireblocks Help Center: About Policies
- Fireblocks Developer Portal: Set transaction authorization policy
A new module that needs its own decision
Illustrative scenario. A payments integration asks the team to enable a module so it can run recurring transfers. The agent can read the module's code and documentation, list the functions it calls on the Safe and draft the enableModule transaction. It cannot decide to enable it.
Enabling and disabling a module are both calls the Safe makes to itself: they go through a transaction signed by the threshold or through a module that is already enabled. Once enabled, the module can act until someone executes disableModule; removing the integration from the team's tools does not remove it from the Safe. Up to Safe 1.4.1, no guard checks what the module executes; on 1.5.0, only a module guard does, and only if one is set. The matrix should therefore treat enabling, the later removal and the guard question as three separate decisions, each with a named person accountable for it.
Check the matrix with negative cases
A matrix is only useful if the tooling around the agent applies it. The first check sorts every draft into a row before anyone signs it. This example uses bash and Foundry's cast; the rules are deliberately short and should follow your own matrix. The function takes the nine fields of the Safe transaction, not only the target and the calldata, because the gas refund fields can also move funds.
# Sort a draft prepared by the agent into the matrix before anyone signs it.
# Requires bash and Foundry's cast. The rules are an example: adapt them to your matrix.
SAFE=0x... # the Safe the agent prepares drafts for
TOKEN=0x... # the only token contract this flow may move
RECIPIENT=0x... # the only recipient approved for this flow
TRANSFER=$(cast sig "transfer(address,uint256)")
ZERO=0x0000000000000000000000000000000000000000
classify() { # arguments: the nine SafeTx fields, in order
[ "$#" -eq 9 ] || { echo "STOP: pass all nine SafeTx fields"; return; }
local to value=$2 data=${3,,} op=$4 selector=${3:0:10}
to=$(cast to-check-sum-address "$1")
if [ "$op" != "0" ]; then
echo "ADMIN REVIEW: operation $op is not CALL; with 1, DELEGATECALL, the target code runs with the Safe's storage"
elif [ "$to" = "$(cast to-check-sum-address "$SAFE")" ]; then
echo "ADMIN REVIEW: call to the Safe itself, which can change owners, threshold, modules, guard or fallback handler"
elif [ "$5 $6 $7" != "0 0 0" ] || [ "$(cast to-check-sum-address "$8")" != "$ZERO" ] || [ "$(cast to-check-sum-address "$9")" != "$ZERO" ]; then
echo "STOP: gas refund fields are set; the Safe would pay a refund to refundReceiver"
elif [ "$value" = "0" ] && [ "$to" = "$(cast to-check-sum-address "$TOKEN")" ] && [ "${selector,,}" = "$TRANSFER" ] && [ "${#data}" -eq 138 ] \
&& [ "$(cast decode-calldata "transfer(address,uint256)" "$data" | head -n 1)" = "$(cast to-check-sum-address "$RECIPIENT")" ]; then
echo "SIGNERS: token transfer to the approved recipient; review the exact payload and amount"
else
echo "STOP: target, function, recipient or value outside the matrix"
fi
}The cases that matter most are the ones that must not pass as a routine transfer: the same payload sent as DELEGATECALL, a call to the Safe that enables a module or changes the fallback handler, an approve call instead of a transfer, a transfer aimed at a contract or a recipient outside the list, and a draft that sets the gas refund fields, which would make the Safe pay a third party on top of the transfer.
MODULE=0x... # module address under review
NOREFUND="0 0 0 $ZERO $ZERO" # safeTxGas baseGas gasPrice gasToken refundReceiver
PAY=$(cast calldata "transfer(address,uint256)" $RECIPIENT 5000000000)
classify $TOKEN 0 $PAY 0 $NOREFUND # SIGNERS
classify $TOKEN 0 $PAY 1 $NOREFUND # ADMIN REVIEW
classify $SAFE 0 $(cast calldata "enableModule(address)" $MODULE) 0 $NOREFUND # ADMIN REVIEW
classify $SAFE 0 $(cast calldata "setFallbackHandler(address)" $MODULE) 0 $NOREFUND # ADMIN REVIEW
classify $TOKEN 0 $(cast calldata "approve(address,uint256)" $RECIPIENT 5000000000) 0 $NOREFUND # STOP
classify $MODULE 0 $PAY 0 $NOREFUND # STOP
classify $TOKEN 0 $(cast calldata "transfer(address,uint256)" $MODULE 5000000000) 0 $NOREFUND # STOP
classify $TOKEN 0 $PAY 0 0 100000 5000 $TOKEN $MODULE # STOPThe second check looks at the chain. When an integration is retired, its module should no longer be enabled, and no module, guard or fallback handler should appear that the matrix does not name. The reads below list what the Safe has configured and then simulate a call from the retired module's address, without sending anything.
export ETH_RPC_URL=... # RPC for the Safe's network
cast call $SAFE "VERSION()(string)"
# Every enabled module, not only the one you expect (first page of up to 10; ask for more if the second value is not 0x...01).
cast call $SAFE "getModulesPaginated(address,uint256)(address[],address)" 0x0000000000000000000000000000000000000001 10
# Transaction guard and fallback handler, read from their storage slots (zero means none set).
cast storage $SAFE 0x4a204f620c8c5ccdca3fd54d003badd85ba500436a431f0cbda4f558c93c34c8
cast storage $SAFE 0x6c9a6c4a39284e37ed1cf53d337577d14212a4870fb976a4366c693b939918d5
# Safe 1.5.0 only: module guard (zero means no guard checks what modules execute).
cast storage $SAFE 0xb104e0b93118902c651344349b610029d694cfdec91c589c91ebafbcd0289947
cast call $SAFE "isModuleEnabled(address)(bool)" $MODULE
# Negative case: an address that is not an enabled module cannot execute through the Safe.
# Expected while the module is disabled: a revert with GS104. A result means it can.
cast call $SAFE "execTransactionFromModule(address,uint256,bytes,uint8)(bool)" \
$SAFE 0 0x 0 --from $MODULEWe ran the three blocks as written, against two public mainnet Safes, versions 1.3.0 and 1.4.1, and on a local fork where the module was enabled and then disabled. The classification matched the comments, and the simulated call returned true only while the module was enabled. On the 1.4.1 Safe, the module list showed an enabled module that the single-address check did not. On the fork, the draft with the gas refund fields set was sorted as STOP; executed anyway, that same draft paid the refund receiver from the Safe's balance on top of the transfer. These checks cover the rules you write; they do not replace a review of the module's code or of the provider's policies.
Keep someone accountable for every change
Every row needs a person who owns it, and every change to the matrix needs the same treatment as a change to the account. Three events should trigger a review: a new Safe version, because guard coverage of modules changes between 1.4.1 and 1.5.0 and the module guard has to be set explicitly; a change to the provider's policies or to who may edit them; and any new module, guard, fallback handler or DELEGATECALL target.
The record should show who approved each change, which negative cases ran and with what result. That record is what lets the team explain later why the agent could do something, or why it could not.
Frequently asked questions
What does the AI agent never do in this permission matrix? The AI agent never signs or submits transactions, holds keys or executes administration changes on the Safe. It only reads, decodes, simulates and drafts.
Who enables a module on a Safe? The Safe owners, with a transaction signed by the threshold, through the administration channel. A module that is already enabled could also do it, and so could the setup call that deployed the Safe, which is one more reason to list every enabled module before the agent gets access. The agent can analyze the module and draft the transaction.
Turn the matrix into a deliverable
The matrix is a working document: one row per capability, named resources, an accountable person, the enforcement point for each rule and the negative cases that test it. Before an AI agent touches a Safe, the team should be able to show which approval each action needs and which test failed when the agent tried to go further.
If you want to discuss the permissions on your own Safe, you can reach us from the contact page; in your message, mention this article and the Safe or flow you have in mind, without keys or confidential operational material.
- Web3 operations with AI: portfolio concept
- AI agents with guardrails, approvals and audit trails
- Web3 Ops Hardening Sprint: approval and role design
- Request a technical review
Source registry
Documentation consulted on 1 October 2026. The sources describe mechanisms and configurations; the matrix, the module scenario and the classification rules are proposals of this article. Executed, with saved output: the three code blocks, with Foundry's cast 1.8.3, against two public mainnet Safes and on a local fork, including the gas refund case; the guard and module test on a local fork of a Safe 1.4.1; and the module guard test on a local fork of a new Safe 1.5.0. Checked against the documentation and not executed: the descriptions of the Turnkey and Fireblocks policy engines.
- Safe: Smart account concepts
- Safe: Modules
- Safe: Guards
- Safe: Fallback handler
- Safe contracts 1.4.1: ModuleManager
- Safe contracts 1.5.0: Safe
- Solidity: Delegatecall and libraries
- Turnkey: Policy overview
- Turnkey: Policy language
- Turnkey: Root quorum
- Fireblocks Help Center: About Policies
- Fireblocks Developer Portal: Set transaction authorization policy
- Foundry: cast reference
Related posts
Need help applying this to your launch or workflow?
Request a technical review and we will map the safest first step.


