DEV Community

Cover image for Did you find a clean way to let the agent commit without giving it write access to the shared config?
Edward Izgorodin
Edward Izgorodin

Posted on Originally published at mnemoverse.com

Did you find a clean way to let the agent commit without giving it write access to the shared config?

Carlos (Bloqarl) asked the question in the title in a dev.to comment under his article about giving coding agents real repositories, and named the trap in the same breath: "I was treating the worktree as the boundary and it's only the checkout."

The short answer is: not by guarding paths inside a shared .git. Isolate the repository, not the paths.

TL;DR

  • A linked worktree shares hooks, config and branch refs with the main repository: with hooks and config read-only, an agent that can commit there can still move main.
  • Use a clone per task that the main repository only fetches from, or let the agent only edit and make the commit outside the box.
  • Make that outside commit run no hooks, because main's own hooks call code from the agent's checkout (husky's npm test, node_modules/.bin first on PATH, lint-staged JavaScript configs).
  • No hooks takes more than --no-verify or an empty hooks directory: hooks declared in git config ran on Git for Windows 2.56.0, and a core.fsmonitor command ran on 2.52.0 and 2.56.0. Run the checks inside the box and read the diff for what runs later.
Setup What the agent can change that runs or moves outside the box
Worktree per task, main .git mounted hooks, config (core.hooksPath; hook.* from Git 2.54.0), branch refs including main
Same, .git/hooks and .git/config read-only branch refs; hook bodies tracked in the tree
Clone per task, main repository fetches on fetch, nothing planted ran; git run inside the clone ran the clone's core.fsmonitor command
Agent edits only, commit outside hook bodies and scripts in the checkout, unless the outside commit runs no hooks

Every run described here was made on one Windows 11 machine on 15 October 2026, with Git for Windows 2.52.0 and 2.56.0, plain git and the tools named. Test scripts planted the hooks and configs; no agent ran.

Why is a worktree per task not a boundary for git?

A linked worktree is an extra working tree added with git worktree add: its own checkout and per-worktree files such as HEAD and the index, everything else shared with the repository.

git's worktree page draws the line. A new worktree is "linked to the current repository, sharing everything except per-worktree files such as HEAD, index, etc." Also: "By default, the repository config file is shared across all worktrees." And: "In general, all pseudo refs are per-worktree and all refs starting with refs/ are shared." Branches live under refs/heads/, so they are shared too. The linked worktree's own .git is a pointer file, so an agent that commits from a container needs the main .git mounted, hooks and config included.

Runs from inside a linked worktree, on Git for Windows 2.52.0 and 2.56.0:

  • A pre-commit hook written to the path git rev-parse --git-path hooks returns ran on a commit in the main worktree.
  • git config core.hooksPath <dir> landed in the shared .git/config, and the hook from that directory ran in the main worktree.
  • git update-ref refs/heads/main <commit> moved the main repository's main to the task's commit.
  • On 2.56.0 only, a configured pre-commit hook written into the shared config ran in the main worktree, also with core.hooksPath pointed at an empty directory.

That leaves the read-only case: hooks and config read-only, refs open for commits. No run enforced read-only protection (chmod a-w on .git/config did not hold on Windows), so that case rests on git's documentation: a commit updates a branch ref, and branch refs are shared.

What do coding-agent sandboxes do with .git?

Two coding-agent sandboxes document how they treat .git. Neither was run here; they are quoted in their own words.

Claude Code's sandboxing page: "Git worktrees: when the working directory is a linked git worktree, the sandbox also allows writes to the main repository’s shared .git directory so commands such as git commit can update refs and the index. Writes to hooks/ and config inside that directory remain denied." The reason for the protected paths: "A command that could edit those files could grant itself permissions, or add a hook or MCP server that Claude Code runs outside the sandbox." Its conditions include "The sandbox is off by default." and "The sandbox covers shell commands only. Claude’s file tools, MCP servers, and hooks run outside it."

OpenAI's Codex approvals and security page: "<writable_root>/.git is protected as read-only whether it appears as a directory or file." And: "If <writable_root>/.git is a pointer file (gitdir: ...), the resolved Git directory path is also protected as read-only." For a linked worktree, git calls that resolved directory the private one under the main repository's .git/worktrees/; the page does not say whether the shared hooks, config and refs are covered.

The first is the read-only hooks and config setup with refs open for commits. Both pages describe what runs inside the sandbox, and the conditions they state matter as much as the paths they protect.

How does a clone per task keep hooks and config inside the box?

git's security section: "Because configuration and hooks are not copied using git clone, it is generally safe to clone remote repositories with untrusted content, inspect them with git log, and so on." In the runs, a test script standing in for the agent planted six hooks, a configured reference-transaction hook and a core.fsmonitor command in the task clone. The main repository's git fetch ../task task:refs/heads/task-review ran none of them and added no hooks and no core.fsmonitor to it.

A fetch still carries some risk, and git says so: "Git does restrict the set of options and hooks that will be run by upload-pack, which handles the server side of a clone or fetch, but beware that the surface area for attack against upload-pack is large, so this does carry some risk."

Never run git inside the agent's clone. The same section: "However, it is not safe to run Git commands in a .git directory (or the working tree that surrounds it) when that .git directory itself comes from an untrusted source. The commands in its config and hooks are executed in the usual way." In the runs, git -C <task clone> status from outside executed the core.fsmonitor command planted in the clone's config. Starting the command outside the box did not change whose configuration it read. To inspect with git, "you should first clone it with git clone --no-local to obtain a clean copy."

Submodules are a second git directory: .git/modules "Contains the git-repositories of the submodules." (repository layout). After git submodule add, .git/modules/lib had its own hooks and config, and a hook placed there ran on a commit inside the submodule.

Why do main's hooks still run the agent's code?

Committing outside with the main repository's own hooks trusts the hook files, not what they call. Each run below used husky 9.1.7, lint-staged 17.6.0, npm 11.13.0 and Node 24.16.0.

  • husky. husky init "creates a pre-commit script in .husky/ and updates the prepare script in package.json", and its example notes "test script will run every time you commit". After npx husky init, a plain commit ran pretest, test and posttest from the checkout.
  • npm. npm test "runs a predefined command specified in the "test" property of a package's "scripts" object" (npm-test). A pre-commit hook kept outside the checkout, with the body npm test, ran all three scripts from the checkout's package.json. The hook file can sit anywhere; the code it calls is the checkout's.
  • PATH. husky's runner sets export PATH="node_modules/.bin:$PATH", and git starts a hook in "the root of the working tree in a non-bare repository." An npm planted in the checkout's node_modules/.bin ran instead of the real one.
  • HUSKY=0. The how-to labels the variable "Temporarily disables all Git hooks", but the check sits in .husky/_/h, inside the checkout, out of git status. A replaced .husky/_/pre-commit ran on a commit made with HUSKY=0.
  • lint-staged. The README: "Writing the configuration file in JavaScript is the most powerful way to configure lint-staged". With npx lint-staged as the outside hook, the top level of sub/lint-staged.config.js executed even with only a root file staged.

A hook from main is as trustworthy as the scripts, configs and executables it calls, and here the agent wrote them.

How can the outside commit run no hooks?

No hooks is not one flag. In these runs, commands at commit time came from the hooks directory, from hooks declared in git configuration, and from core.fsmonitor.

  • --no-verify is narrower than it looks. git-commit describes it as: "Bypass the pre-commit and commit-msg hooks." githooks says prepare-commit-msg "is not suppressed by the --no-verify option", and post-commit "is meant primarily for notification, and cannot affect the outcome of git commit." Both still ran with --no-verify.
  • An empty hooks directory. githooks: "By default the hooks directory is $GIT_DIR/hooks, but that can be changed via the core.hooksPath configuration variable". An empty directory or /dev/null there kept all four hooks of the usual directory from running.
  • Hooks in config, from Git 2.54.0. git-config: "You can also disable all hooks entirely by setting core.hooksPath to /dev/null." On Git for Windows 2.56.0, pre-commit and post-commit hooks in the global config ran anyway, with core.hooksPath at /dev/null and at an empty directory. On 2.52.0, nothing ran.
  • core.fsmonitor. It holds "the pathname of the "fsmonitor" hook command". With one in the repository config, a commit with an empty hooks directory ran the command twice; -c core.fsmonitor=false stopped it.
  • The per-event switch, from Git 2.55.0. hook.<event>.enabled=false stopped a configured pre-commit hook on 2.56.0, and the pre-commit script in the hooks directory still ran. It goes beside an empty hooks directory, not in place of one.

The recipe, as run in Git Bash on both versions on 15 October 2026, against a global config that held a pre-commit hook entry and a core.fsmonitor command:

# outside the box, after reading the diff
mkdir -p /tmp/no-hooks && : > /tmp/empty.gitconfig
export GIT_CONFIG_GLOBAL=/tmp/empty.gitconfig GIT_CONFIG_NOSYSTEM=1
git config --show-origin --get-regexp '^(hook\.|core\.fsmonitor)'   # expect no output and exit code 1
git -c core.hooksPath=/tmp/no-hooks -c core.fsmonitor=false \
    -c user.name="Review Harness" -c user.email=harness@example.invalid \
    commit -m "Agent change, reviewed"
Enter fullscreen mode Exit fullscreen mode

The check printed nothing and returned 1, and the commit ran neither. With the global config emptied, the author comes from -c. If the check prints anything or returns other than 1, stop and read the entries. The check covers hook entries and core.fsmonitor only; it is not a list of every setting that can run a command, which is why the .git the commit is made from stays out of the agent's reach.

Where do the checks run instead?

Inside the box, where the agent's tests already run. Among the inputs Carlos's article lists: "A test can execute code." and "A git hook can run before the commit you asked for." A test that runs the agent's code inside the box is what the box is for. A hook that runs the agent's code on the reviewer's machine, outside the box, is the failure described here.

So checking the change and recording it are separate steps. Lint, tests and formatting run where the agent works; whoever makes the outside commit reads their result and the diff, and the commit itself runs no hooks.

What else in the agent's diff runs or loads later?

A commit that runs no hooks settles what runs at the moment of the commit. It does not approve what the diff will run later, on another machine or in CI. Worth reading first: anything under the hooks path, such as .husky/; package.json scripts with their pre and post companions; lint-staged configs in any directory; CI workflow files. The same article: "Config files are persistence. If the agent can write to your shell config, IDE settings, git hooks or CI workflows, one bad step survives the session."

Agent instructions and memory survive the session too. Anthropic's memory page for Claude Code: "Both are loaded at the start of every conversation. Claude treats them as context, not enforced configuration." A change to CLAUDE.md, AGENTS.md or .claude/ in an agent's diff deserves the same reading as a change to CI.

What this article does not claim

Measurements come from one Windows 11 machine on 15 October 2026. No results from Linux or macOS are reported, and Git 2.54.0 and 2.55.0 were not measured separately on Windows. The sandboxes of Claude Code and Codex were not run: they are described in their own words, not compared. The hook events measured at commit were pre-commit, prepare-commit-msg, commit-msg, post-commit and reference-transaction; other settings that can run a command, such as .gitattributes filter drivers or commit signing programs, were not measured.


Sources and method. Every quotation was read on 25 October 2026 with two instruments, a browser-like request and a reader service with its cache off (GitHub files also as raw files; five passages of git's pages that the reader drops were checked in git's own documentation sources instead); a made-up address on each host answered 404 as a control. The text describes those pages as of that date; the measurements were made on 15 October 2026 and are reported with that date. This article involves no Mnemoverse product, and every run used plain git and the tools named. A longer version, with every run and source, is in the Mnemoverse documentation library: Did you find a clean way to let the agent commit without giving it write access to the shared config?

Edward Izgorodin · Mnemoverse · 2026-10-25

Top comments (0)