Why You Would Want This
You already have Odoo. You already spend time clicking through it. Now you also have an AI agent sitting next to you in the terminal while you work.
Most of the time, that agent helps you with code. But a significant chunk of what you do in Odoo is not complex — it is just tedious. Finding the right project. Looking up a partner. Logging hours. Posting an internal note on a ticket. These tasks require three or four context switches through the UI, and they interrupt your flow at the worst possible moment.
With direct agent access to Odoo, you can end a session like this:
"Log 1.5 hours on task 'Fix API timeout on import endpoint' in the T4B project. Use today's date."
Or:
"Post an internal note on ticket #1142: 'Reproduced the issue locally. Root cause is a misconfigured analytic account. Will fix in next session.'"
The agent figures out the IDs, builds the correct API call, and executes it. You stay in your terminal. The work lands in Odoo without a browser tab.
This guide explains how that works in practice: what the Odoo side looks like, what the CLI helper does, how outbound safety is enforced, and how to replicate the whole setup on your own machine.
Architecture Overview
There are two components:
odoo_agent_helper— a thin Python CLI that sits on your local machine (or CI runner, or wherever your agent runs). It speaks Odoo's JSON-RPC2 API, injects the agent context Odoo needs to identify agent-mode requests, and handles schema introspection and entity caching.strictbase_agent_guard— an Odoo 19 CE module that installs on your Odoo instance. It watches for agent-mode requests and blocks outbound client-facing actions (external emails, invoice sends, external chatter comments) unless a scoped one-time confirmation token is present.
The two components are independent. The helper is not Odoo-specific; it is just a CLI that makes authenticated HTTP calls. The guard module is not agent-tool-specific; it reacts to context flags that any caller can set.
+---------------------------------+
| Agent (Claude Code / Codex) |
| |
| writes JSON spec to /tmp/ |
| calls odoo_local.sh exec-spec |
+----------------+----------------+
| HTTP (JSON-RPC2 API)
v
+---------------------------------+
| Odoo 19 CE |
| |
| context: { agent_mode: true } |
| strictbase_agent_guard active |
| |
| internal actions: pass |
| outbound actions: blocked -----|---> AGENT_CONFIRMATION_REQUIRED
+---------------------------------+
The Agent Helper
What It Does
odoo_agent_helper is a single-file Python CLI (odoo_json2.py) backed by a small cache module (odoo_fast_cache.py). It has no dependencies outside the standard library.
Its job is to give an agent a broad, generic capability surface over Odoo without encoding any business-specific logic. The agent itself maps intent to action; the helper handles transport, introspection, and context injection.
Every call it makes includes two extra context keys:
{
"agent_mode": true,
"agent_channel": "cli"
}
agent_mode is what the guard module checks on the Odoo side. agent_channel is a logging aid so you can distinguish CLI calls from MCP calls or other sources in your Odoo logs.
Available Commands
| Command | Purpose |
|---|---|
call <model> <method> | Execute any JSON-RPC2 model method |
search-read <model> | Convenience wrapper for search_read |
read <model> | Convenience wrapper for read |
create <model> | Convenience wrapper for create |
write <model> | Convenience wrapper for write |
schema-summary <model> | Compact cached field summary for a model |
lookup <kind> <query> | Cached name lookup for partners, projects, tasks, employees |
doc-index | Fetch the full model/method index from Odoo |
doc-model <model> | Fetch full field/method docs for a specific model |
exec-spec | Execute a JSON spec file from /tmp/codex_odoo_request.json |
The exec-spec command is the one agents typically use. The agent writes a JSON file to /tmp/codex_odoo_request.json, then runs:
~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh exec-spec
This gives the agent a stable execution path that does not require shell quoting tricks for complex payloads. The spec format is simple:
{
"action": "create",
"model": "account.analytic.line",
"vals": {
"name": "Fix API timeout on import endpoint",
"date": "2026-03-21",
"unit_amount": 1.5,
"project_id": 18,
"task_id": 142,
"employee_id": 3
}
}
Or for a lookup:
{
"action": "lookup",
"kind": "project",
"query": "T4B"
}
Supported action values: lookup, schema_summary, doc_index, doc_model, call, search_read, read, create, write.
Caching
Two cache files live under .cache/ next to the helper scripts:
schema_summaries.json— model field summaries, TTL 24 hoursentity_lookups.json— partner/project/task/employee name lookups, TTL 12 hours
This keeps schema introspection fast. An agent does not need to fetch doc-model project.task on every call to know which fields exist; the cached summary is available from disk in milliseconds.
The cache is for execution speed only. It does not need to be loaded into the agent's context window unless Odoo work is explicitly requested.
Directory Layout
~/odoo/agent-support/tools/odoo_agent_helper/
|-- odoo_json2.py # main CLI
|-- odoo_json2_common.py # HTTP transport and JSON-RPC2 call logic
|-- odoo_fast_cache.py # schema and entity caching
|-- odoo_local.sh # wrapper for local Odoo instance
|-- odoo_erp.sh # wrapper for production Odoo instance
|-- odoo_local.env-dist # env template for local
|-- odoo_erp.env-dist # env template for production
\-- .cache/
|-- schema_summaries.json
\-- entity_lookups.json
The Odoo Module — strictbase_agent_guard
The Problem It Solves
If you give an agent broad JSON-RPC2 access to Odoo, it can do almost anything a regular user can do. That is mostly fine. Creating tasks, logging hours, writing internal notes — these are low-risk, easily reversible.
But a few Odoo actions are not low-risk. Sending an email to a client. Posting a public chatter comment that notifies external partners. Emailing an invoice. These actions leave your system the moment Odoo executes them. They cannot be undone.
You do not want an agent sending client-facing communication without a human in the loop. But you also do not want to build a narrow allowlist of safe actions — that would make the agent useless for anything not anticipated when the list was written.
The guard module takes a different approach: keep access broad, but intercept the specific outbound paths and require explicit confirmation before proceeding.
What It Guards
When agent_mode is True in the Odoo context, the module intercepts:
mail.thread.message_post— blocks non-note comments with external partner recipients or explicit email recipientsmail.compose.message.action_send_mail— blocks non-log sendsaccount.move.send.wizard.action_send_and_print— blocks invoice sends that include emailaccount.move.send.batch.wizard.action_send_and_print— same, for batch invoice sends
Internal notes (subtype_xmlid = "mail.mt_note") are not blocked. The guard only intercepts calls that would actually reach external parties.
When a guarded action is attempted without a valid confirmation token, Odoo raises a UserError with this marker in the message:
AGENT_CONFIRMATION_REQUIRED reason: outbound_client_communication detail: mail.thread.message_post targets external partner recipients.
The helper on the client side receives this as a RuntimeError containing the JSON error body from the API. The agent can inspect the error, surface it to the human, and ask for approval.
Confirmation Tokens
The strictbase.agent.confirmation model issues scoped one-time tokens. Each token is tied to:
- the issuing user
- the target model name
- the target method name
- the target record IDs
- an expiry time (default: 10 minutes)
Tokens are stored as SHA-256 digests, not in plain text. A token can only be used once; after consumption it is marked as used, and a background vacuum job cleans up consumed and expired tokens.
Token issuance is restricted to users in the Agent Confirmation Approver group. The agent's own API key must not belong to a user in that group.
This is the key trust boundary: the agent cannot authorize its own outbound actions. A separate trusted identity — a human-controlled API key — must issue the token.
On the helper side, this flow is triggered with --confirm-outbound:
~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh call \
res.partner message_post \
--ids 338 \
--confirm-outbound \
--data-json '{"body": "Following up on your support request.", "message_type": "comment", "subtype_xmlid": "mail.mt_comment", "partner_ids": [338]}'
Internally, the helper:
- Uses
ODOO_APPROVER_API_KEYto callstrictbase.agent.confirmation/issue_token - Receives a token
- Injects
agent_confirmation_tokeninto the Odoo context - Retries the original call with the token present
If ODOO_APPROVER_API_KEY is not available in the agent's environment — which is the recommended setup for a real approval boundary — the helper raises an error immediately. The agent cannot self-authorize.
Module Structure
~/odoo/addons/solin/strictbase_agent_guard/
|-- __manifest__.py
|-- __init__.py
|-- models/
| |-- agent_confirmation.py # token issuance and validation
| |-- agent_guard_mixin.py # shared check helpers
| |-- mail_thread.py # message_post guard
| |-- mail_compose_message.py # action_send_mail guard
| |-- account_move_send_wizard.py # invoice send guard
| \-- account_move_send_batch_wizard.py # batch invoice send guard
|-- security/
| |-- strictbase_agent_guard_security.xml # Agent Confirmation Approver group
| \-- ir.model.access.csv
\-- tests/
\-- test_agent_guard.py
Setting It Up
Prerequisites
- Odoo 19 Community Edition
- Python 3.10+ on the machine running the agent
- An Odoo API key for the agent user
- Optionally: a separate Odoo API key for the approver user (required only for outbound confirmation)
Step 1: Create the Agent User in Odoo
Create a regular internal user in Odoo. Give it access to the modules it needs (Project, Timesheets, CRM, Helpdesk, etc.), but do not give it system administrator rights.
Generate an API key for this user: Settings → Technical → API Keys → New.
This is ODOO_API_KEY in the helper config.
Step 2: Install the Guard Module
Clone or copy the strictbase_agent_guard directory into your Odoo addons path, then install:
python3 odoo-bin \ -c /path/to/odoo.conf \ -d your_odoo_db \ -i strictbase_agent_guard \ --stop-after-init \ --logfile=""
Step 3: Create the Approver User (Optional)
If you want to use the outbound confirmation flow, create a second Odoo user and add them to the Agent Confirmation Approver group (Settings → Users → [user] → Permissions).
Generate an API key for this user. This is ODOO_APPROVER_API_KEY.
Keep this key out of the agent's normal environment. If the agent can read it freely, the trust boundary collapses — the agent can issue its own confirmation tokens. The intended pattern is to supply this key only through a human-controlled step (a shell alias that is not in the agent's working environment, or a prompt that injects it at confirmation time).
Step 4: Configure the Helper
cd ~/odoo/agent-support/tools/odoo_agent_helper cp odoo_local.env-dist odoo_local.env cp odoo_erp.env-dist odoo_erp.env
Edit odoo_local.env for your local instance:
ODOO_BASE_URL=http://127.0.0.1:8069 ODOO_DB=your_local_db ODOO_API_KEY=your-agent-api-key-here ODOO_APPROVER_API_KEY=your-approver-api-key-here
Edit odoo_erp.env for your production instance:
ODOO_BASE_URL=https://erp.yourcompany.com ODOO_DB=your_odoo_db ODOO_API_KEY=your-agent-api-key-here ODOO_APPROVER_API_KEY=your-approver-api-key-here
Keep both files out of Git. Add them to .gitignore:
tools/odoo_agent_helper/odoo_local.env tools/odoo_agent_helper/odoo_erp.env
Step 5: Test the Connection
~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh doc-index --pretty
This fetches the model/method index from Odoo. If you see JSON output listing Odoo models, the connection is working.
Try a lookup:
~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh lookup partner "Acme" --pretty
Try a schema summary:
~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh schema-summary project.task --pretty
Step 6: Allow the Wrapper to Run Without Per-Call Confirmations
This is the step most guides skip, and it is the step that determines whether the workflow is actually pleasant or just theoretically possible.
By default, both Claude Code and Codex will prompt you to confirm shell commands that they have not been explicitly authorized to run. If the agent has to ask for permission every time it calls odoo_local.sh, the whole point of the setup is undermined.
The fix is to allowlist the exact wrapper path in your agent's configuration.
Claude Code
In your project's settings.json (or the global ~/.claude/settings.json), add the wrapper path to the allowed commands:
{
"permissions": {
"allow": [
"Bash(~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh*)",
"Bash(~/odoo/agent-support/tools/odoo_agent_helper/odoo_erp.sh*)"
]
}
}
This allows Claude Code to call either wrapper with any arguments without prompting. The actual safety boundary for risky Odoo actions is the guard module on the Odoo side — not the shell permission.
Codex
In your Codex config (typically ~/.codex/config.toml), add a prefix rule for the wrapper:
[[shell.rules]] pattern = ["~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh"] decision = "allow" [[shell.rules]] pattern = ["~/odoo/agent-support/tools/odoo_agent_helper/odoo_erp.sh"] decision = "allow"
If Codex invokes the script through bash -lc, you may also need:
[[shell.rules]] pattern = ["/bin/bash", "-lc", "~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh"] decision = "allow"
The principle is the same either way: approve the one stable command path, keep everything else restricted, and let the Odoo-side guard handle the real safety boundary.
Step 7: Make the Helper Visible to Your Agent
Add instructions to your project's CLAUDE.md (or equivalent context file) pointing at the helper and describing when to use it. Without explicit instructions, the agent may default to raw curl, python3 -c, or improvised shell one-liners — defeating the purpose of having a stable wrapper.
## Odoo access Odoo is accessible via the agent helper during any working session, not just dedicated Odoo sessions. If the current task context makes the client, project, or task reasonably clear, infer and act without asking. Preferred execution path: 1. Write a JSON spec to `/tmp/codex_odoo_request.json` 2. Run: `~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh exec-spec --pretty` For production, use `odoo_erp.sh` instead of `odoo_local.sh`. Do not use raw curl, python3, or subprocess calls for Odoo. Use the wrapper directly. Use `schema-summary` to inspect model fields before writing create/write specs. Use `lookup` to resolve names to IDs before referencing them in specs. Ordinary internal Odoo actions (creating tasks, logging time, posting internal notes) are fire-and-forget — no need to ask first. Ask only on genuine ambiguity or before any client-facing outbound action.
Practical Examples
Here is what this looks like in a real working session. The agent carries context from the conversation forward, resolves IDs with lookup, and executes — without leaving the terminal.
Log Time Against a Task
The agent needs three IDs: employee, project, task. It resolves these with lookup first.
Spec to find the project:
{
"action": "lookup",
"kind": "project",
"query": "T4B"
}
Spec to log time:
{
"action": "create",
"model": "account.analytic.line",
"vals": {
"name": "Fix API timeout on import endpoint",
"date": "2026-03-21",
"unit_amount": 1.5,
"project_id": 18,
"task_id": 142,
"employee_id": 3
}
}
Post an Internal Note on a Ticket
Internal notes are not blocked by the guard — they do not trigger email notifications to external partners.
{
"action": "call",
"model": "helpdesk.ticket",
"method": "message_post",
"ids": [1142],
"data": {
"body": "Reproduced locally. Root cause: misconfigured analytic account. Fix in next session.",
"message_type": "comment",
"subtype_xmlid": "mail.mt_note"
}
}
Search for Open Tasks on a Project
{
"action": "search_read",
"model": "project.task",
"domain": [["project_id", "=", 18], ["stage_id.fold", "=", false]],
"fields": ["id", "name", "user_ids", "stage_id"],
"limit": 20
}
Update a Task's Stage
{
"action": "write",
"model": "project.task",
"ids": [142],
"vals": {
"stage_id": 5
}
}
Confirm an Outbound Email (Full Flow)
This requires ODOO_APPROVER_API_KEY to be available in the shell. In a real setup you would inject it at this step manually:
ODOO_APPROVER_API_KEY=your-approver-key \
~/odoo/agent-support/tools/odoo_agent_helper/odoo_local.sh call \
res.partner message_post \
--ids 338 \
--confirm-outbound \
--data-json '{
"body": "Your ticket has been resolved. Let us know if you need anything else.",
"message_type": "comment",
"subtype_xmlid": "mail.mt_comment",
"partner_ids": [338]
}'
The helper mints a token using the approver key, then retries the call. The guard validates the token against the model, method, and IDs — and allows the single execution.
Design Notes
A few intentional choices worth explaining:
The helper is thin by design. It does not encode business workflows like log_timesheet or close_ticket. The agent maps intent to action. The helper provides transport and introspection. Encoding workflows in the tool layer limits what stronger future agents can do with it.
The guard is narrow by design. It only intercepts the specific outbound paths that carry real risk. It does not try to be a general policy engine. Broader access controls belong in Odoo's permission model, not in an agent-mode layer.
The trust separation is intentional. Keeping ODOO_APPROVER_API_KEY out of the agent's normal environment is the entire point. If the agent can read both keys, it can self-authorize outbound actions, and the guard becomes a speed bump rather than a real boundary. The module enforces the right constraint on the Odoo side; the actual security depends on keeping the approver key out of the agent's reach at the host level.
exec-spec exists for shell quoting stability. Passing complex JSON as inline --data-json arguments is fragile in agent-generated shell calls. Writing to a temp file first eliminates quoting issues entirely.
Conclusion
This setup gives an AI agent functional access to Odoo without crippling it with a narrow allowlist and without giving it unguarded access to client-facing outbound actions.
The result is practical: agents that can do real Odoo work — searching, creating, updating, logging, annotating — and that hit a hard boundary when something would leave your system and reach a client.
The strictbase_agent_guard module and odoo_agent_helper scripts are published by StrictBase. If you have questions or run into issues adapting this to your Odoo setup, reach out at hello@strictbase.com.