fix(git-engine): never run the cloned repo's git hooks (core.hooksPath) - #363
fix(git-engine): never run the cloned repo's git hooks (core.hooksPath)#363orossant wants to merge 1 commit into
Conversation
|
Thanks for putting this together. Preventing repository-owned hooks from running during engine-owned Git operations is the right direction, and this fixes the reported There is one gap I think we should address before merging: Could we centralize this policy in the Git runner by invoking engine Git commands with The underlying change is useful; I think this adjustment would make the implementation match the intended contract and avoid another hook type causing the same production failure. Thanks again for tackling it. |
72bebf7 to
b9d2e47
Compare
|
Revised after review of my own approach. The original version passed It now tries with hooks first, falls back to One implementation note worth flagging: the fallback is placed after |
|
You're right on all three, and the I had argued for a conditional bypass (try hooks, fall back only when they block) on the grounds that a repo's hooks might be protective — secretlint, gitleaks — and that silently disabling them would be worse than the bug. That framing was wrong. Repo hooks are arbitrary untrusted code executing inside the agent runtime with a live credential in the environment, which is exactly what your Also conceded:
Six call-site patches were both incomplete and the wrong layer. I'll rework it as you suggest:
I'll push the revision shortly. Thanks for reproducing both cases rather than just flagging them — the credential path in particular I would not have found. |
A stage that had already produced its artifacts died with
git_commit_failed, and the only detail captured was npm's warnings:
commit_failed — npm warn Unknown project config "strict-peer-dependencies"
npm warn Unknown project config "auto-install-peers"
The cause is the USER repo's own pre-commit hook. A project using husky +
lint-staged installs hooks that shell out to npm/npx, but the runtime
deliberately never installs the checkout's dependencies (see the
inode-budget notes in workspace.js), so the hook exits non-zero and the
stage's completed work is lost for a reason unrelated to that work.
Set `core.hooksPath` to a hook-free path for every engine-owned git
invocation, in runGit — the single choke point all of them pass through,
so operations added later inherit the policy.
Two reasons the engine must not execute repo hooks:
- CORRECTNESS: the failure above. Hooks assuming an installed dependency
tree cannot work in this runtime.
- SECURITY: repo hooks are arbitrary untrusted code running inside the
agent runtime, and pushes carry a short-lived credential in the
environment. A pre-push hook could read AIDLC_GIT_PASSWORD, abort the
push, and surface the value in the returned error detail.
`--no-verify` is NOT sufficient and would have left the same class of bug
open: it covers pre-commit and commit-msg only, so prepare-commit-msg can
still abort a commit and pre-push runs on every push. `-c core.hooksPath`
disables every hook for the invocation without mutating the repository's
own configuration.
Tests drive REAL git in throwaway repos and pin the hooks via
`core.hooksPath` rather than `.git/hooks`, which is the stronger
assertion: it proves the engine's own `-c core.hooksPath` overrides a
hooksPath the repository configured for itself. Cases cover pre-commit,
commit-msg and prepare-commit-msg (commit succeeds, hook never ran, tree
left clean), and a credential-bearing pre-push (push succeeds, the hook
never ran, and the token appears nowhere in the result). All five fail
without the change.
Rebased onto latest main (b9ab0b6).
I confirm the licensing of this contribution under the repository's MIT-0
license.
b9d2e47 to
2570211
Compare
|
Revision pushed (
Tests pin the hooks via Four regression cases, each hook writing a marker file so "did not run" is asserted rather than inferred:
All five fail without the change; I verified by reverting the one-line One thing I did not fold in: the platform still performs no secret scanning of agent-authored content. This PR stops depending on customer hooks, which is right, but the gap remains — happy to open a separate issue for it if that is useful. |
|
First, a big thank you for contributing this and for being so responsive to the review feedback. The revision is a strong improvement: centralizing the policy in I found one remaining path that needs the same treatment before approval. I reproduced both consequences:
Could you route the Git invocations in One small test-strengthening item: the pre-push regression says that the new commit reached the remote, but it only checks that the remote head looks like a SHA. That branch already had a valid SHA before the push, so comparing the remote head directly with Thanks again for the thoughtful turnaround on the first review. This is close, and the updated approach is the right direction. |
Problem
A stage that had already produced all its artifacts died with
git_commit_failed, losing the work. The only detail captured was npm's warnings:The cause is the user repo's own pre-commit hook. A project using husky + lint-staged installs hooks that shell out to
npm/npx. The AgentCore runtime deliberately never installs the checkout's dependencies (see the inode-budget comments inworkspace.jsandrun-stage.js), so the hook exits non-zero and the stage's completed work is lost for a reason that has nothing to do with that work.Observed on a real deployment: the agent successfully created
Business Rules,Domain Entities, andFrontend Components, then lost the whole stage on the commit.Change
Set
core.hooksPathto a hook-free path for every engine-owned git invocation, applied inrunGit— the single choke point all of them pass through, so operations added later inherit the policy automatically.Two reasons the engine must not execute the cloned repo's hooks:
pre-pushhook could readAIDLC_GIT_PASSWORD, abort the push, and surface the value in the returned error detail.--no-verifywould not have been sufficient, and an earlier revision of this PR that used it left the same class of bug open: it coverspre-commitandcommit-msgonly, soprepare-commit-msgcan still abort a commit andpre-pushruns on every push.-c core.hooksPath=...disables every hook for the invocation without mutating the repository's own configuration.Testing
Tests drive real git in throwaway repos and pin the hooks via
core.hooksPathrather than.git/hooks. That is the stronger assertion: it proves the engine's own-c core.hooksPathoverrides a hooksPath the repository configured for itself, not merely the default location.pre-commitcommit-msgprepare-commit-msgpre-pushEach hook writes a marker file, so "never ran" is asserted by the marker's absence rather than inferred. All five cases fail without the change — verified by reverting the one-line
runGitedit and re-running.git-engine.test.js,lane.test.js,resolve-conflict.test.js,run-stage.test.js: 222 passedoxlintclean,oxfmt --checkcleanEngine committed + pushed work for code-generation [unit walking-skeleton], and the execution ran to completion through ~30 stages and opened its PR with zero git failures.Follow-up worth its own issue
The platform does no secret scanning of agent-authored content. This change correctly stops depending on customer hooks, but it does not close that gap — the runtime should scan what its agents commit rather than relying on a hook that may not exist, may not run, or may itself be hostile.
Notes
Rebased onto latest
main(b9ab0b6).I confirm the licensing of this contribution under the repository's MIT-0 license.