Claude Code and Codex¶
Both agents work on the same Python project and run the same checks. There is no separate generated source tree for each agent.
Files and responsibilities¶
| File | Responsibility |
|---|---|
AGENTS.md |
Shared environment, quality, navigation, Git, and approval rules |
CLAUDE.md |
Imports AGENTS.md for Claude |
.claude/ |
Claude permissions, guard hooks, and plugin preferences |
.codex/config.toml |
Codex approval routing and plugin preferences |
.codex/hooks.json |
Codex hook registration |
.codex/rules/ |
Codex prompt rules for publishing commands |
scripts/hooks/guard_config.py |
Protected-files guard for both hosts, one stdlib-only Python script |
.compound-engineering/ |
Compound Engineering settings and committed artifacts |
compound-packs/ |
Project rules that Compound Engineering cites |
This browser server adopted the shared development framework through a reviewed migration. Its Python package, tools, release history and product documentation remain application-owned. To connect Codex as a browser-tool consumer, see Codex client setup.
First setup¶
Run make setup in a terminal before starting either agent. The development
workflow supports Apple silicon macOS, Linux, and 64-bit Windows with WSL, uv,
Git and Make. uv manages Python
3.11–3.13 (3.13 by default), development tools and MkDocs; no separate Node/npm
installation is required. Pyright uses node from PATH when there is one;
otherwise it downloads Node from nodejs.org, so run make typecheck once from
a terminal on a machine without Node.
make setup also fetches the Camoufox browser binary needed for end-to-end tests.
The guard hook uses system /usr/bin/python3 3.9+ in isolated mode (-I -B) so
it works even if project metadata or .venv is broken. Native Windows shell
commands are not configured here; use WSL.
Neither agent runs in an OS sandbox. Claude Code's sandbox block is gone from
.claude/settings.json, and Codex runs with sandbox_mode = "danger-full-access",
approval_policy = "on-request" and approvals_reviewer = "user". bubblewrap
and socat are not prerequisites. What protects the project's configuration is
the protected-files guard below.
For Claude Code, open the repository root and accept project trust. Check
/memory for the imported shared instructions and /permissions for
acceptEdits plus the protected-file ask rules.
make setup installs the plugins .claude/settings.json declares (pyright LSP and
Compound Engineering) in project scope; the CLI rewrites that file, so setup
restores its tracked bytes afterwards. The plugins are optional enhancements;
they are not required for the server or checks.
For Codex, use a CLI release that supports project hooks. Open and trust the
repository, then review and enable the project hooks in /hooks. Hook trust is
tied to the exact configuration: re-trust the hooks whenever .codex/hooks.json
changes. The project enables Compound Engineering in .codex/config.toml, so it
is active only in this project. make setup downloads its marketplace with a
one-command trust override, writing nothing to your global Codex configuration.
Codex itself reads the declaration once you trust the project; restart Codex
after setup.
The project does not edit your global settings. See the official
project instructions
and hook trust documentation.
Start a fresh session and verify that the approval policy is on-request and the
reviewer is user. A running session does not acquire new permissions or hooks
simply because these files were added.
In linked Git worktrees, Codex takes hook registration from the main
checkout. Inspect the source shown in /hooks; changes that exist only on a
worktree branch may not be active yet. Update the main checkout after the
reviewed change merges, then start a new session and review the hooks there.
Everyday permissions¶
| Operation | Claude Code | Codex |
|---|---|---|
Read pyproject.toml / uv.lock |
Allowed | Allowed |
| Edit ordinary source, tests, or docs | Automatic | Automatic |
| Run listed development checks | Allowed by project rules | Automatic |
| Dependency changes | make uv-add, uv-remove, uv-upgrade, uv-lock |
Same targets; the guard accepts the result |
| Formatter output, including metadata | make format* targets; the guard accepts the result |
Same targets |
| Edit a protected file directly | Edit/Write asks; the approved edit is accepted | apply_patch is refused; you make the change |
| Keep a change the guard reverted | make guard-accept-changes, which always asks you |
The same target; Codex prompts you |
| Publish (push, PR create/merge, release) | Always asks you | Prompt rules ask you |
Use the documented Make targets for routine tests, linting, typing and formatting.
make test-one TEST=tests/test_file.py::test_name selects a single test without
exposing arbitrary pytest flags. Pre-push runs make check and make docs-build;
do not repeat those gates manually after edits or before a PR. CI runs real
tool, transport, hook, release, documentation and browser integration scenarios,
plus installed-wheel consumer checks. Run the relevant integration scenario
locally through make test-one when developing that test or its harness.
Dependency changes go through Make, never through uv add, uv remove,
uv lock, uv sync or uv version run by an agent:
| Target | Use |
|---|---|
make uv-add PKG=<spec> [GROUP=<group>] |
Add a dependency; quote a spec that carries an environment marker |
make uv-remove PKG=<name> [GROUP=<group>] |
Remove a dependency |
make uv-upgrade [PKG=<name>] |
Upgrade one package or all of them |
make uv-lock |
Relock after a pyproject.toml edit you approved |
make uv-reinstall |
Delete .venv, rebuild it from uv.lock and restore taplo on linux aarch64 |
make guard-accept-changes |
Keep protected changes made outside an agent call |
The uv-* targets run without approval in Claude Code. Run each as a standalone
foreground command from the repository root. After a target changes
pyproject.toml, run make format before committing if the formatter would
rewrite it.
Publishing asks in both hosts. Claude Code's permission rules ask before git push,
gh pr create, gh pr merge, make bump-* and make release-tag. In a trusted
project, .codex/rules/publishing.rules holds the matching Codex prompt rules, and
approvals_reviewer = "user" sends each prompt to you. The rules match only those
plain command prefixes: a form such as git -C <dir> push bypasses them (the
guard refuses make -C/--directory and these goals combined with other goals
or variables), an untrusted project does not
load them, and a user-level approval_policy = "never" or --ignore-rules
disables them. CODEX_TEST_BINARY=... make test-permissions checks each rule
against the installed CLI.
For a direct metadata change, the agent prepares the exact diff and asks for
approval. Approve that specific operation once. Do not save a blanket shell,
Python, or uv exemption. The approval is the write gate; changing AGENTS.md is
not an alternative to it.
Protected-files guard¶
scripts/hooks/guard_config.py is a stdlib-only script, run as
/usr/bin/python3 -I -B, that both hosts call around every agent tool call. It
keeps the configuration that defines the project's checks and permissions from
changing without you.
What it does¶
For each worktree it keeps a last-approved baseline in
<git-dir>/protected-baseline/. After every agent tool call it compares the
protected files with that baseline and then:
- restores protected files that no approved route changed,
- deletes newly created override configuration files,
- unstages protected content nobody approved, and
- tells the agent that writing these files is forbidden: use the Make targets or ask you.
Hook registration:
| Host | Events |
|---|---|
| Claude Code | PreToolUse, PostToolUse, PostToolUseFailure on Bash\|Monitor\|PowerShell\|Edit\|Write\|MultiEdit\|NotebookEdit\|mcp__.*, plus ConfigChange and Stop |
| Codex | PreToolUse and PostToolUse on Bash\|apply_patch, plus Stop |
At the end of a turn, Stop reports any drift once.
Protected files¶
pyproject.toml,uv.lock,Makefile,scripts/development.mkscripts/hooks/*,scripts/format_files.py,scripts/release.py,scripts/install_taplo.py,scripts/docs_version.py.claude/settings.json,.codex/config.toml,.codex/hooks.json,.codex/rules/*.pre-commit-config.yaml,mkdocs.yml,.taplo.toml,.mdformat.toml,.python-versionAGENTS.md,CLAUDE.md
The guard also deletes these files when they appear, because they would override the project's tool settings:
- At any depth:
ruff.toml,.ruff.toml,pytest.ini,.pytest.ini,pytest.toml,.pytest.toml,tox.ini,setup.cfg, and anypyproject.tomlor.mdformat.tomlbelow the root. - At the root:
pyrightconfig.json,.coveragerc,.coveragerc.toml,GNUmakefile,makefile, the Commitizen files (.cz.toml,cz.toml,.cz.json,cz.json,.cz.yaml,cz.yaml),uv.toml,.gitattributes,taplo.toml,yamlfix.tomland.yamlfix.toml.
Approved routes¶
A change to a protected file stays only if it arrives through one of these:
- An exact, plain
make <target> [VAR=value]from the repository root, in the foreground, for one of:uv-add,uv-remove,uv-upgrade,uv-lock,uv-reinstall,format,format-toml,format-json,format-md,format-yaml,format-project-text,format-ruff,lint-fix,install,setup,install-taplo,bump-patch,bump-minor,bump-majorandguard-accept-changes. - A plain standalone
git commit. Pre-commit's formatting is accepted. A commit chained with other commands or built with$(...)is refused; write the message to a file and rungit commit -F <file>. - Plain
git switch,checkout,pull,merge,rebase,reset,restore,stashorcherry-pick, andgh pr checkout, when the resulting files match the newHEADor content you approved earlier. - A Claude Code Edit/Write on a protected file that you approved through the ask
rules. In Codex,
apply_patchon a protected file is refused: you make the change. make guard-accept-changes, which always asks you (a Claude Code ask rule, a Codex execpolicy prompt rule).
The publishing commands keep their own approval, described under Everyday permissions.
Refused before running¶
The guard refuses these commands before they run:
git commit -n/--no-verify,--no-verifyon any git command, andSKIP=- anything that touches
core.hooksPath, andGIT_CONFIG_*variables - assignments to
PYTEST_ADDOPTS,COVERAGE_RCFILE,MAKEFLAGSandMAKEFILES make -f,-C,-eor--directorypre-commit uninstall- running the guard script directly, or touching the baseline directory
- an allowlisted Make target in the background
- a
git committhat is not a standalone command guard-accept-changes,bump-*orrelease-tagcombined with other goals or variables, which would skip your approval
Patterns are matched against what the shell runs: quoted text and heredoc bodies
are ignored, except code passed to sh -c or eval.
Changes made outside an agent call¶
If you edit pyproject.toml in an editor, or a write lands after its call ended,
the next agent call is refused with "ask the user whether they made this
change". Approve make guard-accept-changes to keep the change. The guard does
not undo a change it did not see being made: to drop it, restore the file
yourself, and agent calls resume once it matches the baseline again. On the very first run the protected files must match
HEAD; if they do not, the same flow applies.
Commits¶
Because a commit must be a standalone call, the protected files it records are
the ones earlier calls left behind, and the guard has already checked those. A
new first pre-commit hook, guard-staged, also fails an agent's commit when it
stages protected content that nobody approved; an allowlisted target such as
make bump-patch may commit its own output. A commit you make outside an agent
call passes. A commit made with Git plumbing that carries unapproved protected
content, including a new file in a protected directory, blocks further agent
calls until you resolve it.
When a merge, rebase or cherry-pick leaves protected files conflicted or
unfinished, the guard does not restore them. It leaves them as they are and
blocks further agent calls until you resolve the conflict, finish the operation
and approve make guard-accept-changes.
Environment and Git hooks¶
The guard also watches the environment that runs the checks:
.venv. It records the name, size and mtime of.venv/binentries,site-packages*.pthfiles,*.dist-infonames and anysitecustomize.py/usercustomize.py. Changes from declared Make targets and Git commands are fine. Any other change refuses further calls until you runmake uv-reinstall, which deletes.venvand rebuilds it fromuv.lock..git/hooks. The hooks are fingerprinted. A change outsidemake setup/make installis restored.
Agent and user settings¶
- Claude Code
ConfigChange. The guard blocks settings that setdisableAllHooks, and removes that key from a local or user settings file on disk. It also blocks project settings that drop the guard hooks. - User-level Codex settings. An agent shell change that sets
hooks = falseorcodex_hooks = false, untrusts the project in~/.codex/config.toml, or breaks~/.codex/hooks.jsonis restored. Your own edits between calls are kept.
Parallel calls¶
A call that can change protected state (an allowlisted Make target, a plain
git commit or other history command, or a protected edit) runs alone. Other
calls started meanwhile are refused with "retry after it finishes". The post-call
check runs when the last running call ends.
Limits¶
The guard is a guardrail against mistakes, not a security boundary. Know what it does not do:
- It cannot stop an agent that deliberately rewrites the guard script. Such a
change shows in
git diff; review it. - Hooks fail open on a timeout. They also skip the guard when the checked-out
commit has no guard, or an older one that does not declare
GUARD_PROTOCOL = 2, so switching to such a commit does not lock the agent out; the guard resumes on the next call after switching back. conftest.pyand.github/workflowsare not protected; review catches changes there.- Codex cannot make approved edits of protected files: you edit them.
- Without an OS sandbox, agents have unrestricted network access and can write outside the repository. The guard protects only the files listed above, the Git hooks and the user-level hook settings.
- An edit inside the files of an already-installed package in
.venvis not fingerprinted. - A write to another worktree surfaces only on that worktree's next call.
- Your
!commands and cancelled calls fire no tool events. Their effects surface at the next agent call as changes made outside a call.
Compound Engineering¶
Compound Engineering's shipped .compound-engineering/config.yaml sends plans and
brainstorm approaches to Opus, implementation units to a Sonnet worker with a
Codex fallback, and cross-model reviews to Codex. Without a sandbox, its detached
jobs (cross-model reviews and external ce-work workers) keep running after the
call that started them. Review diffs, and on fallback implementation code, go to
OpenAI whenever the codex CLI is installed. To keep a project's code away from
it, set cross_model_review_mode: off and work_engine_mode: off in the
untracked .compound-engineering/config.local.yaml.
Verify and troubleshoot¶
The isolated policy tests run in make check. To also check each Codex publishing
rule against an installed CLI, run this from a terminal:
CODEX_TEST_BINARY="$(command -v codex)" make test-permissions
The extra tests make no model/API request and do not alter project metadata.
Codex's legacy sandbox_mode settings and CLI --sandbox switches, and managed
policies, can override the repository's configuration. Check the active session
rather than assuming the repository setting won.
When the guard reverts a change you wanted, or refuses a call because of a change
it did not see, run make guard-accept-changes and approve it. When it refuses
calls because of .venv drift, run make uv-reinstall.
Some work still needs a terminal: the Copier integration matrix (uv downloads missing Python versions), and commits or GitHub CLI calls that need a signing agent or a desktop keyring.
Browser, remote MCP, connector, and already-running shell interactions have their own controls; never use them to bypass a required approval. See Claude permissions.