Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

safer-agent

Experimental. This project is not finished and not proven. Read it before you trust it, and treat every control in it as something to verify rather than to rely on.

It was written with the help of agentic coding tools: mostly Claude, less Codex CLI, and open models through opencode with Ollama for further security assessment. That is worth knowing for two reasons. The tools reviewed their own sandbox, so the blind spots of one may be the blind spots of the review. And the code is what the comments say it is only where somebody checked.

Run Claude Code, Codex, opencode or GitHub Copilot CLI on a Drupal module without giving them your Mac.

safer-agent is four wrapper commands. Each one starts its coding agent inside a Docker container that can edit one module and can do very little else. Your files change. Your machine does not.

safer-claude          # Claude Code
safer-codex           # Codex
safer-opencode        # opencode
safer-copilot         # GitHub Copilot CLI

This project is built for Drupal work. The image is PHP 8.3 command-line with the mbstring and xml extensions. It has no composer, no git and no database driver. The intended shape of a session is one module:

  1. cd into the module you are working on. That folder, and only that folder, is mounted read-write. The agent writes its code there.
  2. Add what the agent must READ: Drupal core, a contrib module, another custom module, a theme, the vendor tree. Use --with-core, --with-modules, --with-themes, --with-contrib, --with-vendor and --with-config for the standard folders, --with-all for the five everyday ones at once, and --add for anything else. Those mounts are read-only. Add -rw to a --with flag to make that folder writable.
  3. Everything else on your Mac stays closed. The site root, settings.php, the files directory and your database are not reachable.

See Working on a module for the paths.


Why

A coding agent runs commands. That is what makes it useful, and it is also the problem. Three things can go wrong, and they are not the same thing:

  1. The agent runs code on your Mac now. The container stops this. It has no privileges, no root, and a read-only filesystem.

  2. The agent writes a file that runs code on your Mac later. A hook in .githooks fires at your next commit. A .envrc fires when you cd into the folder. A postinstall script in package.json fires at your next install. These are files, not commands, so a container does not stop them. dangerous-paths.txt covers the ones that can be covered, and the exit scan reports the ones that cannot.

  3. The agent sends your code out, or fetches instructions in. The container sits on a sealed network. Its only route out is a gatekeeper that allows a short list of hostnames and refuses everything else, and every refusal is logged.

The first is the easy one. Most of this project is the other two.


Requirements

macOS This is what it is built and tested for. It reads the login Keychain once, to start the sandbox's own login, and uses Docker Desktop's path sharing.
Docker Desktop Running. The commands build their own images and never download one.
Bash The commands run under bash, not sh. The stock macOS /bin/bash is enough — they avoid bash 4 syntax on purpose.
The agent itself, installed on your Mac claude, codex, opencode or copilot must be on your PATH. The launcher reads its version and builds an image with the same one, so the sandbox matches what you use.

You do not need to be logged in inside the container. safer-claude and safer-copilot seed a token from your Keychain. See Logging in.


Install

There is nothing to compile and nothing to install into your system.

git clone <this repository> ~/tools/safer-agent
cd ~/tools/safer-agent
chmod +x safer-claude safer-codex safer-opencode safer-copilot

Put the folder on your PATH:

echo 'export PATH="$HOME/tools/safer-agent:$PATH"' >> ~/.zshrc
exec zsh

The commands find their own library, image recipes and lists relative to themselves, so the folder can live anywhere. Keep the parts together: safer-*, lib/, docker/ and proxy/.


Getting started

cd ~/projects/my-site/web/modules/custom/my_module
safer-claude --with-core

The first run builds the agent image and the gatekeeper image. That takes a few minutes. Later runs start in seconds, and rebuild only when the tool on your Mac changes version or when you edit a Dockerfile.

While it starts, it tells you what it did:

Config: throwaway copy — settings changed in the sandbox do not persist.
History for this project: /Users/you/.claude/projects/-Users-you-...-custom-my_module
Placeholders: 29 empty files and folders are created in the
              mounted trees so the agent cannot create them. They are
              removed when the session ends. See dangerous-paths.txt.
              The empty files are hidden from `git status` until then,
              through .git/info/exclude, which the agent cannot reach.
Network: sealed, outbound only through the gatekeeper.
Allowed destinations:
  ^api\.anthropic\.com$
  ^platform\.claude\.com$
  ...

Then the agent starts, and you use it exactly as you always do.

When you quit, it tells you what happened:

No blocked connections this run.
Log: /Users/you/tools/safer-agent/connection_logs/claude-2026-08-27T14-02-11.log

NEW AUTO-RUN CANDIDATES
-----------------------
  New executable files (1):
    /Users/you/projects/my-site/web/modules/custom/my_module/scripts/import.sh

Read that last section before you commit. It lists files that appeared during the session and can run something on your Mac later. Nothing there was blocked — it is a report, not a refusal.

Your first session, in order

  1. cd into the module you want to change — not into the site root. Never run these commands from the launcher's own folder; they refuse it.
  2. Run safer-claude --with-core. Wait for the build.
  3. Work as usual.
  4. Quit. Read the exit report.
  5. Review the diff on your Mac before you commit. That step is not optional — see What this does not protect you from.

Working on a module

The working directory is the one folder the agent can change. Choose the module, not the site.

~/projects/my-site/
├── composer.json                     managed on the host, never in the sandbox
├── vendor/                           --with-vendor   library source, vendored phpunit
├── config/                           --with-config   the config export, read-only
│   ├── sync/                         --with-config-rw  the same, writable
│   └── dev/                                          config_split folders come along
└── web/
    ├── core/                         --with-core     almost every session
    ├── sites/default/settings.php    never mounted. Credentials live here. The
    │                                 launcher only reads the config path from it.
    ├── sites/default/files/          never mounted. User uploads live here.
    ├── modules/                      --with-modules  every module at once
    │   ├── contrib/webform/          --add   when you hook into it
    │   │                             --with-contrib  or all of contrib
    │   └── custom/
    │       ├── my_module/            <- run here. Read-write.
    │       └── my_other_module/      --add   when the two share a service
    └── themes/                       --with-themes   every theme at once
        └── custom/my_theme/          --add   for templates and preprocess work

Start the session in the module:

cd ~/projects/my-site/web/modules/custom/my_module

Then add what the agent must read. The standard Drupal folders have a flag of their own. Anything else takes --add with a path relative to the module:

To give the agent Use
Drupal core --with-core (or --add ../../../core)
Every module, custom and contrib --with-modules
Every theme, custom and contrib --with-themes
All contrib modules --with-contrib
One contrib module --add ../../contrib/webform
Another custom module --add ../my_other_module
One custom theme --add ../../../themes/custom/my_theme
The vendor tree --with-vendor (or --add ../../../../vendor)
The config export --with-config, or --with-config-rw to let the agent export config
Core, modules, themes, vendor and config together --with-all, or --with-all-rw to make them all writable
Any of the above, writable Add -rw to the flag: --with-modules-rw, --with-themes-rw

How the --with-* flags find the folders

The flags are not shortcuts for a fixed number of ../. You may run the command from a sub-folder of the module, or from a theme, and the count would be wrong. So the command finds the folders from the top:

  1. It asks git for the repository root of the working directory. Git runs on your Mac here, never in the sandbox. Without git it walks up the folders until it finds a .git entry.
  2. Under that root it looks for the web root: the web-root value in composer.json first, then web, docroot, html, public_html, public and the root itself. A folder counts only if it holds core/lib/Drupal.php.
  3. If the repository has no web root, for example when a module is its own git repository inside a site, it moves to the repository above and looks again.

The vendor folder is looked for at the repository root, then at the vendor-dir from composer.json, then under the web root. It counts only if it holds autoload.php.

The config folder has no fixed place. Drupal reads it from settings.php, so the command does the same, on your Mac:

$settings['config_sync_directory'] = '../config/sync';

It reads sites/default/settings.php, then settings.local.php next to it, and takes the last line that sets config_sync_directory. Comment lines do not count. The Drupal 8 form $config_directories[CONFIG_SYNC_DIRECTORY] is accepted too. The value may be a plain string, or a string joined with $app_root, DRUPAL_ROOT, dirname(DRUPAL_ROOT), __DIR__ or $site_path. A relative path is relative to the web root, as in Drupal. A value the command cannot read, such as getenv('CONFIG_DIR'), stops it with the line shown. Use --add PATH or --rw PATH with the folder in that case.

The mounted folder is the parent of sync, not sync itself. A site that uses config_split keeps one folder per split next to sync, and the agent needs those too. When that parent would contain the web root or your working directory, only sync is mounted, and a note says so. The command prints the folder it read, and where it read it from:

Config sync: /Users/you/projects/my-site/config/sync   (web/sites/default/settings.php:812)
Read-only: /Users/you/projects/my-site/config   (--with-config)

--with-config-rw mounts the same folder read-write, so the agent can run a config export inside the sandbox. It is --rw on that folder, with the same confirmation: the folder is outside your module, so you type yes once.

Every --with flag has a -rw form that works the same way: --with-core-rw, --with-modules-rw, --with-contrib-rw, --with-vendor-rw and --with-all-rw. Each folder is confirmed on its own, so --with-all-rw asks up to four times. Use the -rw forms only when the agent must change those folders. Core and vendor are managed by composer, so they rarely qualify.

When nothing is found the command stops and tells you. --add PATH always works as a fallback. Each flag prints the folder it resolved to at launch, so you can see what the agent got:

Read-only: /Users/you/projects/my-site/web/core   (--with-core)

--with-modules includes the module you are working in. That is fine: the module folder is mounted read-write on top, and only that folder can be written.

Why these mounts are read-only

Composer owns core/, modules/contrib/ and vendor/, and composer runs on your Mac. A change the agent makes in those trees is either lost at the next composer install or it survives and breaks the next update. Read-only removes the question. The agent can still read a service definition, follow a base class, or check a hook signature — which is all it needs core for.

Use --rw only for a second custom module you are genuinely changing in the same session. The command asks you to type yes, because the folder is outside the project you started in.

What is not reachable, on purpose

  • The site root. Do not mount it. It carries settings.php, the files directory and often a .env.
  • The database. The image has no pdo_mysql and no mysqli, and the network is sealed. So Drupal kernel and functional tests cannot run in the sandbox. Run those on your Mac.
  • composer and drush. Neither is installed. Dependency changes and site commands stay on the host, where you can see them.

What does work inside: php -l for syntax, and a unit-test runner that is already vendored in the project. Mount the tree with --with-vendor, then run it by its full relative path:

../../../../vendor/bin/phpunit tests/src/Unit

Unit tests only. Anything that boots Drupal needs the database.


Usage

safer-claude   [--add PATH] [--with-core] [--with-modules] [--with-contrib] [--with-vendor]
               [--with-config] [--with-all] [--with-*-rw]
               [--rw PATH] [--allow HOST] [--effort LEVEL] [--offline] [-- COMMAND ...]
safer-codex    [--add PATH] [--with-core] [--with-modules] [--with-contrib] [--with-vendor]
               [--with-config] [--with-all] [--with-*-rw]
               [--rw PATH] [--allow HOST] [--offline] [-- COMMAND ...]
safer-codex    --history | --merge-history
safer-opencode [--add PATH] [--with-core] [--with-modules] [--with-contrib] [--with-vendor]
               [--with-config] [--with-all] [--with-*-rw]
               [--rw PATH] [--allow HOST] [--ollama] [--offline] [-- COMMAND ...]
safer-copilot  [--add PATH] [--with-core] [--with-modules] [--with-contrib] [--with-vendor]
               [--with-config] [--with-all] [--with-*-rw]
               [--rw PATH] [--allow HOST] [--offline] [-- COMMAND ...]
safer-copilot  --history

Anything that is not one of these flags is passed to the agent itself, so safer-copilot --reasoning-effort high or safer-codex --model o3 work as they would natively.

Flag What it does
--add PATH Mount another folder read-only. --ro is the same flag. This is the normal way to give the agent Drupal core, a contrib module, a theme or the vendor tree. Repeat it for each one.
--with-core Mount <web root>/core read-only. The web root is found from the repository root, so the flag works from any folder inside the site. See How the --with-* flags find the folders.
--with-modules Mount <web root>/modules read-only: every custom and contrib module.
--with-themes Mount <web root>/themes read-only: every custom and contrib theme.
--with-contrib Mount <web root>/modules/contrib read-only.
--with-vendor Mount the composer vendor folder read-only.
--with-config Mount the config export folder read-only. Its place is read from sites/default/settings.php on your Mac. The parent of the sync folder is mounted, so config_split folders come along.
--with-all --with-core, --with-modules, --with-themes, --with-vendor and --with-config in one flag. Contrib is left out because it is already inside modules. A site with no themes folder skips that part.
--with-*-rw Any --with flag with -rw added mounts that folder read-write: --with-config-rw lets the agent export config, --with-all-rw makes all five folders writable. Each folder is confirmed like --rw.
--rw PATH Mount another folder read-write. The agent can change your Mac through it. Use it only for a second custom module you are really editing. A folder outside your project must be confirmed by typing yes.
--allow HOST Allow one more destination, for this run only.
--offline No network at all. The strongest mode, and the right one for a read-only analysis pass.
--effort LEVEL safer-claude only. low, medium, high, xhigh or max. Overrides /effort for the whole session, so leave it off for interactive work.
--ollama safer-opencode only. Reach the Ollama server on your Mac. Inference and read-only queries pass; model downloads, uploads and deletions are refused.
--history safer-codex and safer-copilot. List the saved sandbox sessions per module and stop. No container starts.
--merge-history safer-codex only. Copy every saved sandbox session into your Mac's ~/.codex, so the native codex resume --all lists them. Copies only; existing files are left alone.
-- COMMAND ... Run something else instead of the agent. -- bash gives you a shell in the sandbox, which is the fastest way to see what the agent can see.

Environment variables

SAFER_DEBUG=1 Print the assembled docker run arguments, one per line, then run.
SAFER_DEBUG=2 Print them and stop. Nothing starts.
SAFER_ASSUME_YES=1 Answer the --rw confirmation. For unattended runs only.

Examples

All of these are run from inside web/modules/custom/my_module.

# The everyday session: your module, plus core to read.
safer-claude --with-core

# The same, written out. --with-core is the same mount as this.
safer-claude --add ../../../core

# Extending a contrib module. Core, the contrib module, and the vendor tree.
safer-claude --with-core --add ../../contrib/webform --with-vendor

# The whole site as reference: core, every module, and vendor.
safer-claude --with-core --with-modules --with-vendor

# The same plus the config export, in one flag.
safer-claude --with-all

# Config work: the agent exports config into the sync folder.
safer-claude --with-core --with-config-rw

# Two custom modules that share a service. The second one is editable too.
safer-claude --with-core --rw ../my_other_module

# Twig and preprocess work: the theme is read-only reference.
safer-claude --with-core --add ../../../themes/custom/my_theme

# A review pass over the module. Nothing needs the network.
safer-claude --offline --with-core

# Look inside the sandbox yourself, and see exactly what the agent can see.
safer-claude --offline --with-core -- bash

# opencode against a local model, with no internet at all.
safer-opencode --ollama --offline --with-core

What is protected, and how

Your machine, during the session

The container runs with --cap-drop ALL, --security-opt no-new-privileges, a read-only root filesystem, and your own user id rather than root. There is no --privileged and no Docker socket. Git is removed from the image and the build fails if it comes back.

Your machine, after the session

Two different mechanisms, because the problem has two halves.

The tool's own config is a throwaway copy. The container gets a copy of ~/.claude (or ~/.codex, ~/.config/opencode, ~/.copilot), and only what genuinely has to be shared is bound back. Your Mac never reads that copy, so a hook the agent writes into it dies with the session. This is default-deny: anything not explicitly shared is simply not shared.

The module folder is a list. A copy will not work here, because the point of the mount is that the agent's work reaches your Mac. So the auto-run paths inside a code tree — .githooks, .envrc, .vscode, .npmrc, Makefile, project-level agent settings, CI definitions — are covered read-only, and the absent ones are pre-empted so the agent cannot create them either. Every --add and --rw tree gets the same treatment, so a mounted core/ or modules/contrib/ folder is covered as well, and .git is masked in all of them. The exit scan then runs over the writable trees only, because those are the only ones that can gain a file.

That second half is a blocklist, and a blocklist is never finished. So the session ends with a scan that reports what a list cannot cover: new files deep in the tree, names nobody predicted, and changes to files the agent must be allowed to edit.

Your data

The container is on an internal Docker network with no route out. A gatekeeper container (tinyproxy, default-deny) is the only door, and it allows a short list of hostnames per tool. DNS goes through it too, which closes DNS tunnelling. Refusals are logged to connection_logs/; successful connections are deliberately not recorded.

The container also cannot reach your Mac's own services, sibling containers, or your VPN.


What this does not protect you from

Read this part. The rest of the README is the good news.

  • Code you run yourself. If the agent edits a source file and you later run that file, that is execution you opted into. Only review catches it.
  • Prompt injection from code already in the repository. Mounted code may carry instructions aimed at the agent. Nothing here changes that.
  • Files the agent must be allowed to edit. package.json and composer.json can carry install scripts, and a module can ship its own. They are reported at exit, never covered. Install with npm ci --ignore-scripts and composer install --no-scripts as a habit.
  • Drupal code that runs on your site. A .module file, a hook, an event subscriber or a Twig template runs the next time you load a page or clear the cache. The sandbox has no database and no web server, so nothing runs there. It runs when you point your site at the module.
  • Deep paths and unpredictable names. A .vscode/tasks.json written five folders down, or a something.code-workspace invented mid-session, cannot be pre-empted. They appear in the exit report instead.
  • Symlinks. The agent can leave a symlink in your project that points somewhere else. A later host process that follows it acts on the target.
  • Anything you put in a prompt. That reaches the model provider under your own account.
  • A GitHub token that can write. safer-copilot is the one command whose model API lives at a host that also publishes: api.github.com takes gists, issues and pull-request comments, and the allowlist cannot refuse it. The network does not make that token harmless; its permissions do. Use the fine-grained token described under Logging in, and nothing wider.

The single habit that covers most of this: read the diff on your Mac before you commit, and read the exit report first. Use git status --ignored — the agent can write .gitignore.


Logging in

safer-claude gives the sandbox its own login, separate from your Mac's. The token lives in history/claude/credentials.json inside the launcher folder. Each run seeds it from there, and saves the renewed token back at exit. You log in once, inside the sandbox; after that a run starts authenticated, also after a quit and a relaunch later in the day.

The first run, or a run after you delete that file, seeds from your macOS Keychain instead. If Claude Code still asks for a login, do it once: run /login inside the sandbox. There is no browser in the image, so use the manual code paste. The result is saved at exit.

Why not the Keychain every time: a token renews about every eight hours, and a renewal makes the old refresh token useless. A renewal inside the sandbox left the Keychain with a dead token, so every run needed a new login. The sandbox's own file fixes that. Your Keychain is read once and never written, and it is never reachable from the container.

Several sandboxes at once share that one token, like two Claude Code windows on your Mac share one Keychain entry. At exit the newest token wins, so a sandbox that did not renew cannot overwrite one that did. Only a session of over eight hours renews at all. If two such sessions run side by side, the second renewal can fail; that costs one /login. A sandbox killed with kill -9 loses its renewal; the next run uses the older token, which usually still renews.

safer-copilot also seeds a token from the Keychain, but not the one copilot login stores. That is an OAuth token with the repo and gist scopes, and api.github.com is on the allowlist because the model API lives at GitHub too. So that token would let the agent publish your code to a gist or a public issue under your name. Instead you create a token that can only ask the model questions, once:

  1. On GitHub, under Settings, Developer settings, create a fine-grained personal access token. Give it one permission, under Account permissions: Copilot Requests. No repository access.

  2. Store it in your login Keychain. You are asked for the value; it is not shown and it does not land in your shell history:

    security add-generic-password -a "$USER" -s safer-copilot-token -w

The launcher reads that entry at each start and hands it to the container through a file that only the docker command reads. It warns if the token is not a fine-grained one. When the token expires, delete the entry and add the new one. There is no /login inside the Copilot sandbox: it needs github.com, which the allowlist refuses on purpose.


Layout

safer-claude                  one command per tool. Each holds only that
safer-codex                   tool's own flags, config strategy, image
safer-opencode                and allowlist.
safer-copilot

lib/safer-common.sh           the sandbox machinery, shared by all four.

dangerous-paths.txt           paths the agent may not write, in any mounted
                              tree.                        <- edit this one

docker/Dockerfile.claude      one agent image per tool. The first three are
docker/Dockerfile.codex       nearly identical and differ in the npm package
docker/Dockerfile.opencode    installed last. Copilot is one native
docker/Dockerfile.copilot     executable, so its image has no Node.js.
docker/Dockerfile.proxy       the gatekeeper (tinyproxy).  Shared.
docker/Dockerfile.forwarder   the Ollama forwarder (nginx). opencode only.

proxy/allowlist-common.txt    destinations for all four.   <- edit this one
proxy/allowlist-claude.txt    that tool's own service only
proxy/allowlist-codex.txt
proxy/allowlist-opencode.txt
proxy/allowlist-copilot.txt
proxy/allowlist-custom.txt    your private additions, all four. Optional,
                              not committed.
proxy/tinyproxy.conf          gatekeeper configuration
proxy/ollama.conf             the forwarder's default-deny rules

connection_logs/              <tool>-<timestamp>.log, the newest 30 per tool
connection_logs/README.md     how to read them

history/claude/credentials.json  the sandbox's own Claude login, renewed at
                              the end of every run. See "Logging in".
history/codex/<module slug>/  Codex and Copilot session history, one folder
history/copilot/<module slug>/  per module. Ignored by git, apart from the
history/README.md             README.

Configuring it

Two files are meant to be edited. They are shaped differently on purpose.

proxy/allowlist-*.txt is an allowlist. Anything not named is refused, so it fails safe when it is out of date. Add an anchored pattern such as ^www\.example\.com$. Read the rules at the top of allowlist-common.txt first — some hosts are dangerous to allow no matter how reputable they look.

dangerous-paths.txt is a blocklist. Anything not named is permitted, so it fails open when it is out of date. That is why it carries a long comment about what it does not cover, and why the exit scan exists.

Every entry in dangerous-paths.txt names a path and how to treat it:

hide   dir    .githooks                   # invisible and unwritable
pin    file   .envrc                      # readable, not writable
warn   file   package.json                # untouched; reported if written

Where to read more

Nothing in this project is documented only here. Each file explains itself, and the explanations are where the code is.

Read this For
dangerous-paths.txt The write-protection list, the format, the three holes a list cannot close, and which entries depend on the operating policy. Start here — it is the most important file to understand.
lib/safer-common.sh § 7 How paths are covered, and why covering a file that exists is only half the job.
lib/safer-common.sh § 7b The exit scan: what it looks for and how it measures.
lib/safer-common.sh § 8 --add and --rw, and the confirmation.
lib/safer-common.sh § 8b The --with-* flags: how the repository root, the web root, the vendor folder and the config folder are found.
lib/safer-common.sh § 9 The config strategy essay: why a throwaway copy beats a read-only mount, and why the hook risk disappears rather than being contained.
lib/safer-common.sh § 10 The sealed network, the gatekeeper, and the connection log.
lib/safer-common.sh § 13 When an image rebuilds, and why the check is not version-only.
docker/Dockerfile.claude What is in the image, and why git is purged twice and then asserted absent.
docker/Dockerfile.copilot Why the Copilot image downloads one executable instead of an npm package, how the download is checked, and why its package cache is unpacked at build time.
safer-copilot The token question: why a GitHub token is not like the other tools' tokens, and which one to create. Also what the launcher adds to the copilot command line.
proxy/allowlist-copilot.txt Copilot's hosts, and why github.com stays refused even though the model API lives at GitHub.
docker/Dockerfile.forwarder Why the Ollama path needs nginx and not a byte-copying relay.
proxy/allowlist-common.txt The rules for widening the network, including which reputable hosts are still a bad idea.
proxy/ollama.conf Which Ollama endpoints are permitted, and which are refused.
connection_logs/README.md How to read a log, how to decide whether to allow a host, and what the logs cannot tell you.
connection_logs/*.log What each run tried to reach and what appeared in your project.
history/README.md Where Codex and Copilot session history and the sandbox's Claude login are kept, why they are here, and how to list, merge or delete them.
safer-claude Why the sandbox has its own login instead of using the Keychain each time, and how several sandboxes share it.

Known limits

These are recorded properly in the files that own them; this is the short list.

  • The image has to be rebuilt when you upgrade the tool on your Mac. The command does it automatically, which means an upgrade makes one session start slowly.
  • Settings changed inside the sandbox do not persist. That is the config strategy working, not a bug.
  • opencode session history is per-session and disappears with the container. safer-claude keeps this project's transcript in ~/.claude/projects and the sandbox's login in history/claude/credentials.json. safer-codex keeps this module's sessions in history/codex/ inside the launcher folder, so codex resume works across runs. Your native Codex does not see them until you run safer-codex --merge-history. The prompt history file and the SQLite index are not kept; Codex rebuilds the index from the session files, so the first codex resume of a run can be slow. safer-copilot does the same in history/copilot/, so copilot --resume works across runs; there is no merge command for it.
  • safer-copilot is the least tested of the four. It was written from the Copilot CLI executable and GitHub's documentation, and the image was not built on a Mac before it was committed. Expect the first run to show a refused host or two in the connection log, and read proxy/allowlist-copilot.txt for what each one is.
  • The Copilot image has no Node.js, because Copilot CLI does not need it. An MCP server configured with npx cannot start inside that sandbox.
  • Copilot CLI's own file-access check is told about every mounted folder with --add-dir, so it does not ask before reading core/. That flag also loads any .github/skills and .github/agents found there; those are prompts.
  • A resumed Codex conversation remembers excerpts from folders you mounted last time, even if you leave the --add off this time. Mounts and network always come from the current command line, never from the transcript.
  • A session transcript sits in a folder the agent can write. Read it as agent output, not as a record of what happened.
  • A run killed with kill -9 skips the exit trap, so the placeholder files stay in your project, the .git/info/exclude entries that hid them stay too, and the exit scan does not run. Each run writes a record of both to sessions/ before it starts, and deletes it when it ends normally. Run safer-claude --repair (any of the four commands) to undo what a dead run left, or just launch again: every launch does the same repair first. The repair also removes the dead run's generated files in proxy/ and its containers and network in Docker. See sessions/README.md.
  • No database driver, so Drupal kernel and functional tests cannot run in the sandbox. php -l and unit tests work; everything else is a host job.
  • No composer and no drush. The agent cannot add a dependency, apply a patch or clear a cache. It edits code, and you run the site commands yourself.
  • Cloud-backed Ollama models make their outbound connection from your Mac's daemon, not from the container, so the gatekeeper never sees that traffic and the log cannot record it. safer-opencode names any such model at startup.

A note on the operating policy

Some of this depends on a habit rather than on code, and the files say so where it matters:

  • The agents are never run natively in a real project folder. On the Mac they are started only in empty folders, to change settings, update hooks or install plugins.
  • Only code is mounted, and the read-write mount is a single module. Core, contrib and vendor go in read-only when they are needed at all.
  • Secret-bearing paths are never mounted: sites/default/settings.php, sites/*/settings.local.php, sites/default/files/, .env, ~/.ssh.
  • The site root itself is not mounted, so a mount cannot pick those up by accident.
  • Mounted code is treated as untrusted and is reviewed before anything runs on the host.

dangerous-paths.txt groups its entries by whether they depend on the first of these. If the policy changes, that section tells you what becomes load-bearing again.

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages