Security¶
New in v1.6.0
- The sandbox API key: on Odoo 14.0 to 19.0,
scripts/deploy_local.pyhas Odoo create an API key for the sandbox'sadminand keeps it, never printed, in the sandbox's.env. See Local infrastructure. - The copies check and the options check (
--check-copiesand--check-options) run no program, contact nothing and never print a sensitive value, whatever the input holds. See Credentials. - Known limit 10 is closed, but for one case: on Odoo 10 to 12, a context key with the name of one of the model's fields is read as a value. See Permission policy.
New in v1.5.1
- A session-start hook that reads no plugin option and no credential:
hooks/session_start.pyreplaceshooks/check_options.py. See Credentials. - The server reads only the credentials that the plugin options pass to
it: it no longer reads
ODOO_HTTP_BEARER. - Local diagnostics, metadata only, checked against an allowlist. See Privacy.
- What the plugin directory's checks report, and where to verify each finding.
- A new known limit of the policy: a context passed positionally on Odoo 10 to 12 (see Permission policy).
New in v1.5.0
- Permission policy on top of the Odoo user's own rights,
with the
standardprofile by default. See Permission policy below. - Local audit trail, hash-chained, with no values and no secrets.
- Sessions keyed by the whole identity: the session key includes a per-process HMAC of the secret, and the header-based multi-tenant path was removed.
odoo_connectionsno longer returns theuser:pass@of the URL.- The options hook also checks the two policy options.
Credentials¶
- As a plugin, credentials are plugin options (
/plugin→ odoo-tools → Configure options). The API key and password aresensitiveoptions: stored in the operating system's secure credential store, never insettings.jsonor your shell profile, and passed only to the local MCP server process, which sends them only to the Odoo URL you configured. Do not exportODOO_*in your shell for the plugin. - The plugin has two
SessionStarthooks, and neither contacts anything:- the first runs
python3 --versionand prints a warning only whenpython3is not runnable (the MCP server could not start); - the second,
hooks/session_start.py(new in v1.5.1, it replaceshooks/check_options.py), starts only whenpython3is 3.9 or newer and reads no plugin option and no credential. It reads onlyCLAUDE_PLUGIN_DATAandCLAUDE_CODE_ENTRYPOINT(the name of the Claude app that started the session), the event's source and session id, the end of the server's diagnostics (diagnostics/server.jsonland its rotatedserver.jsonl.1), whether the container mode's marker exists, its daily timestamps and the plugin's version: nothing outside the plugin's folders. When the last 14 days record neither a successful connection to Odoo nor a server start with every connection setting, it says where the Odoo tools run (not in Cowork or Claude chat), at most once a day at the start of a session, and tells Claude, with one diagnostics line for the doctor (on the first session after an install or an upgrade, and wherever it has no data folder, it tells Claude only); when the last server start was locked by the permission policy, it says so with its E-code. It writes one metadata line per session todiagnostics/hook.jsonl(never through a symbolic link), keeps two timestamp files for the daily notices and never stops the session (it always exits 0). With the container mode of/odoo-tools:deploy-localit says nothing.
- the first runs
- Claude Code exports every saved plugin option, sensitive ones included, to
the environment of each plugin hook process as
CLAUDE_PLUGIN_OPTION_*. This plugin's hooks read none of them (checked byserver/tests/test_product_rules.py). - The copies check and the options check (
run_stdio.py --check-copiesand--check-options, which the guided route and/odoo-tools:odoo-doctorrun in Claude Code) read on stdin the JSON that a Claude Code command prints (claude plugin list --jsonorclaude plugin configure <id> --json): they run no program, open no file of Claude Code's, contact nothing and never print a sensitive value, whatever the input holds. Of the Odoo URL, the options check repeats only the scheme, host and port, never a path, a query or a fragment, which can hold a portal link's token, and it refuses a URL with any@as holding a user name or password, without showing any part of it. Both read UTF-8, also from a Windows pipe. See Two copies of odoo-tools. - In standalone mode (Docker,
python -m odoo_mcp), the server reads credentials from the environment (ODOO_URL,ODOO_DB,ODOO_LOGIN,ODOO_API_KEY/ODOO_PASSWORD). The server never takes an Odoo URL or secret from a client request: the header-based multi-tenant path was removed in v1.5.0, andODOO_MULTITENANTis parsed but reserved. The server reads no other credential:ODOO_HTTP_BEARER, which it read from its environment but never used, was removed in v1.5.1. - The MCP server does not persist secrets to disk or write them to logs.
Sessions are keyed by
url|db|loginplus a per-process HMAC of the secret, so the same URL, database and login with another secret never reuses an authenticated session; the secret itself is never part of a key or a log line. - The
Settingsobject redacts credentials inrepr()/str()so that an accidental log does not leak them (guaranteed bytests/test_security_redaction.py). - Prefer API keys (Odoo >= 14) over passwords. Use least-privilege Odoo users and turn on Read-only mode in the plugin options for exploration or demos.
- Which data is processed and which services are contacted: see Privacy.
The fallback CLI does write a file with credentials
Unlike the MCP server, the fallback CLI
(/odoo-tools:odoo-setup-cli) stores your credentials in a local .env
file in ~/.claude/tools/odoo-cli. The setup creates it readable only by
your user, with placeholders: you paste the secret yourself in your own
editor; Claude never asks for it in the chat. If you use that surface: keep
those restrictive permissions (equivalent to chmod 600), do not commit
it to version control, and delete it when you stop using the CLI. Rotate the API
key with the same cadence you would apply to any other secret stored on disk.
Dedicated user for the agent
Create a specific Odoo user for the agent (for example
ai-bot@yourcompany.com) with only the permissions it needs. Generate the API
key from that user and rotate it periodically. This limits the blast radius if
the key leaks.
What happens to your company's data¶
It is worth distinguishing two data paths:
- From the MCP server to Odoo (direct). The server runs on your infrastructure and talks directly to your Odoo instance over JSON-2 / JSON-RPC / XML-RPC. Transgenia does not act as an intermediary: we do not proxy, we do not store, and we do not see your data or your credentials.
- From the AI agent to the model provider. When you operate Odoo through an agent (Claude Code or another MCP client), the results of each tool (the Odoo records you query) are returned to the client and sent to the model provider (for example Anthropic, in the case of Claude) so the model can process them. This is inherent to any workflow with AI agents; it is not something this plugin adds.
Practical implications: you decide what you query and with which user of Odoo (least privilege). Review your model provider's data-handling policies and avoid bringing sensitive fields the agent does not need. For regulated data, query only what is essential and consider anonymizing at the source.
Supply chain¶
- Zero runtime dependencies. Since v1.2.0, the MCP server uses only the
Python standard library (the MCP protocol and the JSON-2, JSON-RPC and XML-RPC
transports are implemented on it). The plugin launches it with
python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py: no package launcher (uv,uvx,npx), no install step and no downloads at start. The code that runs is exactly the readable code shipped in the plugin folder. run_stdio.pyputs the bundled source first onsys.path, so anotherodoo_mcppackage installed on the machine cannot shadow the reviewed code.- The optional extras (
cache,metrics,otel) are only used if you install them (standalone mode); the plugin installs none. - The optional fallback CLI is the only piece that installs packages: it runs
npm installwhen you set it up with/odoo-tools:odoo-setup-cli.
What the plugin directory's checks report¶
The Claude plugin directory lists these findings for this plugin. Each one, with its name as the directory shows it, and where to verify it:
- Uses a credential from the user's machine
(
.claude-plugin/plugin.json,server/src/odoo_mcp/config.py). The MCP server reads two credentials, the Odoo API key and the Odoo password, from the environment that.claude-plugin/plugin.jsonfills from thesensitiveoptionsodoo_api_keyandodoo_password. It sends them only to the Odoo URL you configured, the service they belong to: the three transports inserver/src/odoo_mcp/transport/connect to that URL only and refuse redirects.server/tests/test_product_rules.pyfails if the server reads any other credential from its environment. scripts/deploy_local.pygenerates random passwords (secrets.token_urlsafe) for the local Docker sandbox of/odoo-tools:deploy-local, keeps them in an.envreadable by your user alone in the sandbox folder, and uses them only with that sandbox's containers on127.0.0.1. They are not your credentials. On Odoo 14.0 and newer it also has the sandbox's Odoo create an API key for its administrator user (odoo shellin a one-off container of the sandbox, with Odoo's ownres.users.apikeys), checks that the key signs in, and keeps it in the same.envasODOO_API_KEY, never printed. You copy it into the plugin's Odoo API key option; Claude never reads it.- Tests (
server/tests/): they set fake canary credentials to prove that no log line, audit line, diagnostics record, support report or hook output ever contains a credential. The plugin never runs them. - Uses hooks. The two
SessionStarthooks of Credentials:hooks/session_start.pyreads the plugin's data folder andCLAUDE_CODE_ENTRYPOINTonly, never a plugin option or a credential (checked byserver/tests/test_product_rules.py), and always exits 0; the other one only runspython3 --version. - Runs a local program for an MCP server.
python3 ${CLAUDE_PLUGIN_ROOT}/server/run_stdio.py: standard library only, nothing installed or downloaded (see Supply chain). - Contains a download-and-run command. Only two commands download and run
code on your computer, and only when you invoke them and confirm:
/odoo-tools:deploy-localpulls and runs Docker images (Transgenia's unchanged copies of the official PostgreSQL and Odoo images, by series tag, and the MCP server image at the plugin's version), and/odoo-tools:odoo-setup-clirunsnpm installfor the optional CLI. The MCP server and the hooks download and install nothing.
Automatic checks (CI)¶
The .github/workflows/security.yml workflow runs on every push/PR:
- gitleaks: scans history and diffs for committed secrets. It is built
with
go installat a fixed tag (v8.18.4, every module verified against the Go checksum database), with nocurldownload-and-run of binaries. - leak-guard: looks for forbidden internal or tenant markers (customer data, infrastructure hosts, tax identifiers, hardcoded credentials) and fails the build if they appear. The repository is vendor-neutral: it contains only the generic tooling plus the public brand/authorship and the services offering. The organization-specific markers are read from a repository secret rather than written in the workflow (the plugin folder is the repository root, so the workflow reaches every installer), and matches on them are reported by file name only.
.gitignore: excludes.env,*.env, virtual environments, and build output so that local credentials are not committed by accident.
Transport¶
- Use Odoo URLs with HTTPS. JSON-2, JSON-RPC and XML-RPC all run over the
URL you provide; a plain
http://URL is sent unencrypted. - JSON-2 (Odoo saas~18.4 / 19.0+) sends the API key as
Authorization: beareron each request, never on a redirect (redirects are refused), sends no cookies, and never shows Odoo's server traceback (thedebugfield of an error) in a tool result. It accepts API keys only, so a password configuration keeps the legacy endpoints. - All transports verify the TLS certificate (hostname included) against the
system trust store (honoring
SSL_CERT_FILE; if the optionalcertifipackage is installed, its CA bundle is used too). There is no option to turn verification off. All of them applyODOO_TIMEOUT(120 s by default; Request timeout plugin option). - JSON-RPC does not follow HTTP redirects (a redirected POST would become a body-less GET); configure your Odoo's final URL.
- Basic auth in front of Odoo:
ODOO_URLmay carryuser:pass@; JSON-RPC sends it as anAuthorizationheader and no error, log line or tool result echoes the password (since v1.5.0,odoo_connectionsstrips theuser:pass@from the URL it reports). JSON-2 needs that same header for its API key, so such a URL stays on the legacy endpoints. Odoo URL is not asensitiveoption: that password is stored in the plugin settings, not in the secure credential store. - The server logs only to stderr; stdout is reserved for the MCP channel.
Audit trail¶
Since v1.5.0, every tool call, refusals included, leaves one JSON line: the
tool, the model and operation names it touched (op:model[:method]), the layer
and rule of a refusal, counts, the duration, the exception class name of an
error and the policy digest. It never holds argument values, domains, field
values, ids, file contents, the URL, tokens, keys, passwords or error message
texts; a name that fails the policy's name check is written as ?. The Odoo
database and login appear only on the start line written when the server
starts.
- The plugin writes daily files (
odoo-tools-audit-YYYY-MM-DD.jsonl) to its data folder (ODOO_AUDIT_DIR), readable by your user alone (folder0700, files0600, never through a symlink), and keeps them 90 days (ODOO_AUDIT_RETENTION_DAYS;0keeps them). - Without a folder, the lines go to the
odoo_mcp.auditlogger on stderr;ODOO_AUDIT_DIR=offwrites none. - Each line carries a hash of the previous one, and
python3 server/run_stdio.py --verify-audit FILEnames the first line that was edited, reordered or inserted, or an incomplete final line. From one file alone it cannot detect whole trailing lines removed from a day or a deleted day file (the surviving prefix still chains). Locally this is evidence, not prevention: whoever can write the folder can rewrite the files. - A failure to write never breaks a tool call.
Details and format in Audit trail.
Permission policy¶
In the local (stdio) mode the permission policy limits what Claude can do through the odoo-tools MCP tools. It protects against mistakes and against instructions hidden in data Claude reads. It does not limit the person who holds the API key, and it is not a sandbox for an agent that has unrestricted shell access on the same machine. The boundary that holds against everyone is the Odoo user itself: give the agent a dedicated, least-privilege Odoo user (not an administrator) and an API key with an expiry. In a future remote mode, where the server keeps the Odoo credential, the same policy becomes real enforcement.
- What it is. A ceiling on top of Odoo's access rights, record rules and
field groups, set by the options Permission profile (
readonly,standardby default,builder,full) and an optional Permission policy file, read once at start from an explicit absolute path (never from a default location). It never grants what Odoo refuses and is never weaker than Read-only mode.fullkeeps the 1.4 behavior. Reference: Permission policy. - What
standardcloses. Stored secrets (system parameters, mail-server, LDAP and payment-provider credentials, IAP and bank-sync tokens, CFDI certificates and keys, the outgoing mail queue) cannot be read; users, groups, access rules, modules, bank accounts, partner merges, chatter history and fiscal sequences are read-only; the models that send email, SMS, letters or share links, and the outbound methods (message_postexcept internal notes,message_subscribe,send_mail, and so on), are closed; server actions, automations, views and otherir.*records are read-only; deletes are refused; credential-like fields are withheld from reads and refused in arguments.fullstill refuses creating, changing or revoking API keys and uninstalling modules and, to enforce that, a call whose arguments or related models the policy cannot check. - Two layers. The declared accesses of each tool are decided from its arguments before any session exists (Layer A: nothing reaches Odoo). Then every Odoo call the tool makes goes through a guarded session (Layer B) that checks it again with its values and paths (x2many commands, domain and export paths, the parent of an attachment, a user's contact email) before it is sent, and stops as a bug any call the tool did not declare. A tool without a permission mapping cannot be registered, listed or called.
- An invalid policy locks the server: only
odoo_versionworks, and it explains the error without contacting Odoo. - Seatbelt, not sandbox. In stdio, an agent with an unrestricted shell can
read the server's environment, edit the policy file or the plugin options,
or call Odoo directly. Claude Code
permissions.denyrules on the policy folder, Bash approval prompts and the instructions line (a policy refusal is final) make that harder, not impossible.odoo_versionwarns when the Odoo user is an administrator, because Odoo then narrows nothing. - Known limits (detailed in Known limits): side
effects inside business methods (a confirmation email, existing
automations) are Odoo's; strings returned by methods (a portal URL with a
token) are not redacted; pointer models other than
ir.attachmentmatter only once a file refuses business reads; related fields with harmless names can carry withheld data; many2one links are not checked as touches; published content is an outbound channel by effect; counts and aggregates allow inference;builderequalsfullfor confidentiality, because automations run code on the server; on Odoo 10 to 12, a key of an object passed positionally to a business method outside the policy's table that is the name of a field of the model is read as a value, so it is not held to the context allowlist.
Read-only mode¶
- With Read-only mode (
ODOO_READONLY=1) the server no longer lists the tools that are not read-only (odoo_create,odoo_write,odoo_unlink,odoo_execute,odoo_translate_set, both Studio-style tools and both import tools:odoo_import_previewtoo, because its rolled-back dry run can still consume sequence numbers and fire webhooks), and refuses them with a clear error before touching Odoo when one is called by name. Reads, exports,odoo_telemetry_previewand the read-only Online tools keep working. No permission profile or policy file can bring a write back while it is on. - The fallback CLI has no equivalent, nor a permission policy: its
writecommand always attempts the call. Use a least-privilege Odoo user there. odoo_online_profilelists the user's API keys by name, scope and dates only: Odoo keeps no key material in a readable field, and the plugin never creates, rotates or revokes keys.odoo_record_documentsreturns a file only for an attachment of the record asked for, and only up tomax_bytes(at most 5 MiB).
Local infrastructure (/odoo-tools:deploy-local)¶
- It runs only when you invoke it and confirm; no hook runs Docker.
- Odoo is published on
127.0.0.1only; it is not exposed to your network. - Passwords are generated locally with Python's
secrets, stored in~/.odoo-tools/sandbox/.env(0600, folder0700) and never printed; Claude does not read that file. - On Odoo 14.0 and newer, the API key that Odoo creates for the sandbox's
adminis kept in that same file asODOO_API_KEYand is not printed either, not even in an error message. You copy it into the plugin's Odoo API key option; never paste it in the chat. Odoo lists it as "odoo-tools sandbox" under Preferences > Account Security; if you remove it there, the nextsandbox upcreates another. - Images come from
ghcr.io/transgenia(unchanged copies of the official ones, refreshed weekly) with a Docker Hub fallback. - The
ghcr.io/transgenia/odoo-mcp-toolsimage runs as an unprivileged user and receives credentials through--env-file, never on the command line.
Reporting a vulnerability¶
Write to dev@transgenia.org with details and reproduction steps. Please do not open public issues for security reports. We aim to acknowledge receipt within 5 business days.