Skip to content

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

TL;DR

Not by guarding paths inside a shared .git. Isolate the repository, not the paths.

  • 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.
SetupWhat the agent can change that runs or moves outside the boxMeasured on Git for Windows
Worktree per task, main .git mountedhooks, config (core.hooksPath; hook.* from Git 2.54.0), branch refs including main2.52.0 and 2.56.0 (hook.* on 2.56.0)
Same, .git/hooks and .git/config read-onlybranch refs; hook bodies tracked in the treerefs: not measured read-only, the case rests on git's shared-ref documentation; hook bodies: 2.52.0 and 2.56.0
Clone per task, main repository fetcheson fetch, nothing planted ran and main gained no hooks and no core.fsmonitor; git run inside the clone ran the clone's core.fsmonitor command (and runs its hooks, per git's security section)2.52.0 and 2.56.0
Agent edits only, commit outsidehook bodies and scripts in the checkout, unless the outside commit runs no hooks2.52.0 and 2.56.0, with husky, npm and lint-staged

This library is published by Mnemoverse, which makes a persistent memory API for AI agents. Nothing on this page involves it, and every run described here was made with plain git and the tools named.

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.

The hooks directory is where git looks for scripts named after events such as pre-commit: $GIT_DIR/hooks, unless core.hooksPath points elsewhere.

A configured hook is a hook command declared in git configuration (hook.<name>.command with hook.<name>.event) instead of a script in the hooks directory, documented from Git 2.54.0.

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

The question came from Carlos (Bloqarl) in a dev.to comment that also names the trap: "I was treating the worktree as the boundary and it's only the checkout." (comment) The distinction behind it is between a separate checkout and a separate repository.

git's worktree page draws that 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 (every hook and script wrote a marker to a log; "nothing ran" means an empty log with git exit code 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. On 2.52.0 the same entries ran nothing.

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

What do coding-agent sandboxes do with .git? ​

Two coding-agent sandboxes document how they treat .git, each quoted here in its 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." Its protected paths include "In your working directory only: shell startup files such as .bashrc and .zshrc, .gitconfig, the .vscode and .idea directories, and hooks and config inside .git". The reason: "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." The conditions stated there: "The sandbox is off by default." "On native Windows, Claude Code runs commands unsandboxed." "The sandbox covers shell commands only. Claude’s file tools, MCP servers, and hooks run outside it." And: "Unsandboxed retries: Claude can ask to run a command unsandboxed, usually after it fails in the sandbox"; "The retried command runs unsandboxed."

OpenAI's Codex sandboxing page on workspace-write: "The agent can read files, edit within the workspace, and run routine local commands inside that boundary. This is the default low-friction mode for local work." On tools: "If the agent runs tools like git, package managers, or test runners, those commands inherit the same sandbox boundaries." Under the on-request approval policy, "The agent works inside the sandbox by default and asks when it needs to go beyond that boundary." Its approvals and security page: "In the default workspace-write sandbox policy, writable roots still include protected paths:" among them, "<writable_root>/.git is protected as read-only whether it appears as a directory or file." Also: "If <writable_root>/.git is a pointer file (gitdir: ...), the resolved Git directory path is also protected as read-only." And: "Protection is recursive, so everything under those paths is read-only."

The first is the read-only hooks and config setup with refs open for commits; the second protects .git, and the Git directory a .git pointer file resolves to, 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. Both describe commands that run inside the sandbox, and both name a way for a command to run outside it.

Neither sandbox was run; the measurements on this page are plain git.

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 runs on Git for Windows 2.52.0 and 2.56.0, 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 to it and no core.fsmonitor to its config, and a git clone --no-local of the clone had no hooks and no core.fsmonitor.

A fetch still carries risk, and git states both halves: "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." On the upload-pack page: "upload-pack tries to avoid any dangerous configuration options or hooks from the repository it’s serving, making it safe to clone an untrusted directory and run commands on the resulting clone." The same page adds that running upload-pack as another user is platform dependent; on many systems: git clone --no-local --upload-pack='sudo -u nobody git-upload-pack' (not run here).

Never run git inside the agent's clone. git's security 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 runs on Git for Windows 2.52.0 and 2.56.0, git -C <task clone> status from outside executed, twice, 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). On Git for Windows 2.52.0 and 2.56.0, 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. Each submodule's repository needs the same treatment.

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, with the same result on Git for Windows 2.52.0 and 2.56.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". In the 9.1.7 source the script is the package manager's name plus test (npm by default), and core.hooksPath is set to .husky/_. 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), with pretest and posttest around it (scripts). A pre-commit hook kept outside the checkout, with the body npm test, ran all three from the checkout's package.json. The hook file can sit anywhere; the code it calls is the checkout's.

PATH. husky's runner .husky/_/h sets export PATH="node_modules/.bin:$PATH" (husky 9.1.7 source), and git starts a hook in "the root of the working tree in a non-bare repository." With main's .husky copied outside the checkout and -c core.hooksPath=<copy>/_, an npm planted in the checkout's node_modules/.bin ran instead of the real one.

HUSKY=0. husky: "It can be globally disabled (HUSKY=0) or be opt-in if wanted", and the how-to labels the variable "Temporarily disables all Git hooks". The check [ "${HUSKY-}" = "0" ] && exit 0 sits in .husky/_/h, inside the checkout, and husky's .gitignore of * there keeps that directory out of git status. A replaced .husky/_/pre-commit stayed out of git status --short and ran on a commit made with HUSKY=0.

lint-staged. The README: "You can also place multiple configuration files in different directories inside a project." "For a given staged file, the closest configuration file will always be used and tasks will by default run in the directory of the config". And: "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 with sub/a.txt staged, and again with only the root file r.txt staged. The closest-file rule chooses the config for a staged file; it did not keep a config in another directory from running.

A hook from main is as trustworthy as the scripts, configs and executables it calls, and here the agent wrote them. These tools run project code by design; the question is whose code it is.

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. The recipe at the end of this section deals with all three.

--no-verify is narrower than it looks ​

git-commit describes the flag as: "Bypass the pre-commit and commit-msg hooks." githooks says prepare-commit-msg "is not suppressed by the --no-verify option", and that post-commit "is meant primarily for notification, and cannot affect the outcome of git commit." Both still ran with --no-verify on Git for Windows 2.52.0 and 2.56.0.

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". git-config calls pointing it at /dev/null "usually only advisable for expert users and on a per-command basis using configuration parameters of the form git -c core.hooksPath=/dev/null". On Git for Windows 2.52.0 and 2.56.0, 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-hook: "when instructions suggest adding a script to .git/hooks/<hook-event>, you can specify it in the config instead", and "The default <hook-name> from the hookdir is run last." 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 with core.hooksPath at /dev/null and at an empty directory. On 2.52.0, nothing ran.

Empty the global and system config ​

GIT_CONFIG_GLOBAL and GIT_CONFIG_SYSTEM: "Take the configuration from the given files instead from global or system-level configuration." GIT_CONFIG_NOSYSTEM: "Whether to skip reading settings from the system-wide $(prefix)/etc/gitconfig file." With both emptied and an empty hooks directory, a configured hook in the repository's own config still ran on Git for Windows 2.56.0, and nothing ran once that entry was removed. In the edit-only route, whoever makes the outside commit owns that local config and checks it.

Check what is left ​

bash
git config --show-origin --get-regexp '^(hook\.|core\.fsmonitor)'
git hook list pre-commit   # Git 2.54.0 and later

The first line prints each entry with its file, or nothing with exit code 1 (Git for Windows 2.52.0 and 2.56.0). git hook list will "print a warning and return 1" when no hook is configured; on Git for Windows 2.52.0 it answered error: unknown subcommand: `list' with exit code 129, which is not a clean result.

Not only hook.*: core.fsmonitor ​

core.fsmonitor holds "the pathname of the "fsmonitor" hook command". With one in the repository config, a check for '^hook\.' alone printed nothing, and a commit with an empty hooks directory ran the command twice, on Git for Windows 2.52.0 and 2.56.0. The wider pattern above shows it, and -c core.fsmonitor=false stopped it. The check is not a list of every setting that can run a command, which is why, in the edit-only route, the .git the commit is made from stays out of the agent's reach.

The per-event switch, from Git 2.55.0 ​

hook.<event>.enabled=false means "no hooks fire for that event, regardless of any per-hook hook.<friendly-name>.enabled settings." On Git for Windows 2.56.0 it stopped a configured pre-commit hook, and the pre-commit script in the hooks directory still ran; with an empty hooks directory as well, nothing ran. It goes beside an empty hooks directory, not in place of one.

The recipe, as measured ​

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.

bash
# 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"

The block was run as written in Git Bash on Git for Windows 2.52.0 and 2.56.0 on 15 October 2026, against a global config that held a pre-commit hook entry and a core.fsmonitor command: the check printed nothing and returned 1, and the commit ran neither. The check covers hook entries and core.fsmonitor only; it is not a list of every setting that can run a command.

The control, the same global config with only an empty hooks directory, ran the core.fsmonitor command on both versions and the configured hook on 2.56.0.

Where do the checks run instead? ​

Inside the box, where the agent's tests already run. Among the inputs the article behind the question 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 this page describes.

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, for example:

  • 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: "CLAUDE.md files: instructions you write to give Claude persistent context." "Auto memory: notes Claude writes itself based on your corrections and preferences". "Both are loaded at the start of every conversation. Claude treats them as context, not enforced configuration." Its sandbox denies writes to the files Claude Code loads configuration from because, as quoted above, a command editing them could add a hook or MCP server that runs outside the sandbox. A change to CLAUDE.md, AGENTS.md or .claude/ in an agent's diff deserves the same reading as a change to CI. See Why does my code editor AI keep overriding custom repository guidelines every session? and Memory Poisoning: The Patient Path to Your API Keys.

What this page does not claim ​

Measurements come from one Windows 11 machine with Git for Windows 2.52.0 and 2.56.0, husky 9.1.7, lint-staged 17.6.0, npm 11.13.0 (the npm pages quoted are the v12 documentation) and Node 24.16.0, on 15 October 2026. No results from Linux or macOS are reported here, and Git 2.54.0 and 2.55.0 were not measured separately on Windows. Test scripts planted the hooks and configs; no agent ran. The sandboxes of Claude Code and Codex were not run either: they are described in their own words, not compared, and nothing here states how either one handles update-ref, .husky/, package.json or CLAUDE.md.

The hook events measured at commit were pre-commit, prepare-commit-msg, commit-msg, post-commit and reference-transaction. Lists of files and events here are examples. Other settings that can run a command, such as .gitattributes filter drivers or commit signing programs, were not measured, so the configuration check in the recipe covers hook entries and core.fsmonitor only. Every quotation was read on the dates given in Sources.

Common questions ​

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

Not by guarding paths inside a shared .git: a linked worktree shares hooks, config and the branch refs under refs/, so even with hooks and config read-only an agent that can commit there can 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 with no hooks, because a hook body such as husky's npm test runs code from the agent's checkout.

Do git worktrees share hooks and config with the main repository? ​

Yes. A linked worktree shares everything with the main repository except per-worktree files such as HEAD and the index, including the config file by default and the branch refs under refs/. In runs on Git for Windows 2.52.0 and 2.56.0, a hook, a core.hooksPath setting and a ref update made from a linked worktree all took effect in the main worktree.

Does git commit --no-verify skip every hook? ​

No. git-commit says it will "Bypass the pre-commit and commit-msg hooks." In runs on Git for Windows 2.52.0 and 2.56.0, prepare-commit-msg and post-commit still ran with --no-verify.

Does core.hooksPath=/dev/null turn off all git hooks? ​

It turns off the hooks directory. git-config says "You can also disable all hooks entirely by setting core.hooksPath to /dev/null." On Git for Windows 2.56.0, hooks declared in the global config (documented from Git 2.54.0) still ran with that setting, and on 2.52.0 and 2.56.0 a core.fsmonitor command ran with an empty hooks directory. Empty the global and system config, check what is left, and add -c core.fsmonitor=false.

Can husky or lint-staged run code the agent wrote, even with the hooks from main? ​

Yes. The hook body npm test runs pretest, test and posttest from the checkout, husky 9.1.7 puts node_modules/.bin first on PATH, and lint-staged loaded a JavaScript config from a directory with no staged files. Each was run on Git for Windows 2.52.0 and 2.56.0 with husky 9.1.7, lint-staged 17.6.0 and npm 11.13.0.

Is it safe to run git commands inside the agent's clone? ​

No. git says "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." In runs on Git for Windows 2.52.0 and 2.56.0, git status pointed at the clone from outside executed a core.fsmonitor command planted in the clone's config. Fetch from the clone, or inspect a copy made with git clone --no-local; git notes that upload-pack, which serves a fetch, limits what it runs but has a large attack surface.

Sources ​

Every quotation was read on 15 October 2026 and again on 16, 17 and 18 October 2026, each time by a direct request and by a second reader; on each host, a request for an address that does not exist was the control, and it answered 404. Quotations are contiguous text of the page named, with its punctuation.

git (git-scm.com, latest-version pages)

husky

lint-staged

npm (v12 documentation; the measured npm was 11.13.0)

Anthropic, Claude Code

OpenAI, Codex

The question and the article

Edward Izgorodin · Mnemoverse · 2026-10-18

Mnemoverse is a persistent-memory API for AI agents. Free key: console.mnemoverse.com · Plans and limits · Docs: Getting Started