I use coding agents across real repositories every day. I run a small security firm, and between audits and two kids there are never enough hours, ...
For further actions, you may consider blocking this person and/or reporting abuse
Separate worktrees per task reads as isolation, and it isolates the checkout only: a git worktree shares its hooks and its config with the main repository, and git hooks are already on the list under "config files are persistence".
The git-worktree documentation says a linked worktree shares everything except per-worktree files such as HEAD and the index, and that the repository config file is shared across all worktrees by default. From inside a linked worktree, git rev-parse --git-path hooks points into the main repository's .git/hooks.
It takes a few minutes to check. On git 2.52, a pre-commit hook written from the task worktree to the path git reports ran on the next commit made in the main worktree. So did a hook kept in a directory inside the task worktree, once core.hooksPath was pointed at it by absolute path from there, since that setting lands in the shared config. In that second case the only write outside the task worktree is a git config command. As controls, a commit in the main worktree before the hook was written ran nothing, a commit after unsetting core.hooksPath ran nothing, and a file written into the task worktree's checkout did not appear in the main one.
The box does not close this by itself. The .git in a linked worktree is a one line file pointing at the main repository's .git/worktrees directory, so with only the worktree mounted git answers "not a git repository", and an agent that is expected to commit needs the main .git mounted as well, hooks and config with it. Keeping .git/hooks and .git/config outside what the agent can write is what turns the per task worktree into a boundary for execution and not only for files.
This is a really good catch, thank you. I was treating the worktree as the boundary and it's only the checkout. Hooks and config are shared, so a hook written from the task worktree runs on my next commit in the main one. Same "config files are persistence" problem, just one level up. Moving .git/hooks and .git/config out of what the agent can write. Did you find a clean way to let the agent commit without giving it write access to the shared config?
Carlos, not by guarding paths inside the shared .git. Read-only hooks and config close the two paths that run code on the next commit in the main worktree, but the same directory holds the branches, and the git-worktree page says every ref under refs/ is shared across worktrees. An agent that can commit through it can also point main at its own commit with git update-ref. Submodules add one more path, since each keeps its repository under .git/modules with hooks and config of its own.
A clone per task gives the agent its commits without any of that. The SECURITY section of git(1) says configuration and hooks are not copied by git clone, so whatever the agent writes into its own .git runs only inside the box, and the main repository fetches the task branch from outside. That fetch runs upload-pack in the task clone. The same section says upload-pack restricts the config and hooks it honors there but warns that its attack surface is large, and the git-upload-pack page shows it run as another user through --upload-pack, an option git fetch takes too.
The shorter route is an agent that never commits: it edits the checkout, and the harness commits from outside the box.
Neither route covers hooks that live in the tree. Husky points core.hooksPath into .husky and keeps the hook bodies as tracked files such as .husky/pre-commit, so an edit there runs on the next commit in any checkout of that branch where husky is installed, and in the second route that commit is the harness commit itself, outside the box. Committing with the hooks of main instead of the ones in the checkout, and reviewing any diff under the hooks path the way a change to CI is reviewed, closes it.
Thanks again, this is gold. The refs point is the one I hadn't thought about: if it can commit, it can move main. I like the shorter route best, the agent never commits, it only edits, and the commit happens from outside after I read the diff. That fits how I already treat its diff like a PR from a stranger. And good call on husky, if the harness commits with the hooks from the checkout, a planted .husky/pre-commit runs outside the box. Committing with main's hooks and reviewing anything under the hooks path like a CI change makes sense.
Main's hooks close the hook body but not what the body calls, so "closes it" at the end of my last reply was too strong. The pre-commit that husky init writes runs npm test, and npm test runs the test script from the package.json in the checkout, and through it the test files the agent wrote. That is the "a test can execute code" line from your list, only now running outside the box. lint-staged, the other common hook body, takes its commands from whichever config file sits closest to each staged file in the checkout, and that file can be a JavaScript module that runs when lint-staged loads it.
On git 2.52 this shows up in one commit: with core.hooksPath pointed at a directory outside the checkout whose pre-commit was npm test, the commit ran a test script written into the checkout. With --no-verify that script did not run, but prepare-commit-msg and post-commit still did, and githooks says the first one is not suppressed by that flag. With core.hooksPath pointed at an empty directory, nothing ran.
So the commit made from outside is cleanest with no hooks at all, core.hooksPath pointed at an empty directory for that one command, and the checks the hooks were there for run inside the box, which is where running the agent's tests belongs anyway.
Good correction, thanks. So the hook is just a launcher and whatever it calls is still the agent's code. Empty hooksPath for the outside commit and running the tests inside the box makes sense, that's where they belong anyway.
Didn't know prepare-commit-msg still fires with --no-verify, that one would have got me.
The taint-analysis framing is the right mental model, and it's underused. The path people miss most is the self-referential one: the agent generates code or a tool result, then reads its own output back in a later step and treats it as trusted because "it came from me." A prompt-injected issue doesn't have to reach a shell directly — it can convince the agent to write a setup script that the agent then runs two steps later. The untrusted text and the execution sink are separated in time, so it doesn't look like injection in any single step.
Your three mitigations line up with ours: box it, no standing secrets, approvals that actually gate the install/write/CI/network sinks. The one I'd add is egress control — even a read-only, sandboxed agent can exfiltrate via a crafted URL in a "helpful" curl. Blocking outbound-by-default to anything that isn't the package registry closes the quietest path. How are you handling network egress inside the box?
The self-referential one is a really good point. The injection and the execution end up two steps apart, so no single step looks bad. That's partly why I treat the agent's own diff like a PR from a stranger, including the scripts it wrote itself.
On egress: right now it's the approval gate, anything that goes to the network waits for me. Default-deny with only the package registry allowed is cleaner though, because it doesn't depend on me noticing the tenth prompt. Are you doing that with a proxy inside the container or at the firewall level?
Đúng trọng tâm. Mình thấy nhiều team vẫn treat agent như "junior dev siêu tốc" — cho full repo access, hy vọng nó tự sửa bug production. Thực tế agent không có context về business logic, threat model, hay những quyết định architecture được trade-off năm xưa. Mình đã từng xem agent refactor toàn bộ auth module vì "code smell" mà bỏ qua rate-limiting custom viết cho compliance — may mà CI catch được.
Thay vì trust-by-default, mình enforce: agent chỉ đọc file liên quan đến task (định nghĩa qua spec), write-access gated qua PR review bắt buộc, và mọi change chạm security-sensitive path (auth, crypto, secrets handling) phải có human sign-off. Còn việc inject context — dùng summary document thay vì dump cả repo vào context window, vừa tiết kiệm token vừa giảm hallucination.
Một lesson đắt giá: agent giỏi pattern matching, kém reasoning về "why this exists". Document quyết định architecture (ADR) quan trọng không kém code (site: labagent .tech)
Thanks! "Good at pattern matching, bad at why this exists" is a great way to put it. That's why writing down why a weird check exists matters so much, the agent can't see the incident behind it.
On approvals without fatigue: we tier actions into auto-run / single-confirm / hard-block, and only tier-1 ever fires without a human in the loop. The bigger unlock was batching — grouping related approvals into one review step cut the flood more than any prompt tweak did. Curious whether you have landed on tiering too, or found another way to keep the prompt count sane.
Yup, tiering is pretty much where I ended up too. Read-only goes through, and installs, network, CI and hooks wait for me. Hard-block is a good addition, some things should never even be a prompt, like anything near signer keys.
Batching I haven't tried. How do you group them, per task or per type of action?
Curious how you handle outbound network access in the sandbox. Keeping secrets out is a good start, but an install script or test can still make arbitrary requests.
Do you restrict egress to known destinations, or rely on approvals for network access?
Right now it's approvals, anything that goes to the network waits for me. But you're right that install scripts and tests are where it slips through. When I approve "run the tests" I'm not really approving network access, even if a test makes a request. Default deny with only the package registry allowed is where I want to end up. Have you found a clean way to do that without breaking postinstall scripts that download binaries?