fix(hooks): resolve .. in the main-checkout guard #172

Merged
claudiomaradonna merged 1 commit from fix/guard-dotdot into main 2026-10-29 19:52:03 +01:00
Collaborator

Why

The main-checkout guard let an agent write into a main checkout on its
main branch when the path climbed back with .. through a directory
that does not exist, such as /tmp/x/../../repo/AGENTS.md, while it
refused the same file named directly. A mistaken path join or injected
text was enough to change the checkout AGENTS.md keeps as it was.
Fixes #169.

Refusing every path with .. was weighed and set aside: it would also
refuse edits the guard passes today, such as ../sibling/x from a
linked worktree.

What changed

The guard now resolves . and .. in the part of the path that does
not exist yet, where every name is a missing directory and so resolves
exactly, and a leading .. climbs from the existing directory as the
kernel does. It then looks the repository up where the path really
lands, so both paths above are refused. Empty names from a doubled
slash are dropped first, so missing//../.. resolves the same way.
Paths that do not land in a guarded checkout pass as before.

How to verify

In a scratch directory, make a repository on main and run the guard
with a path that climbs back into it:

git init -q -b main repo && git -C repo commit -q --allow-empty -m init
mkdir outside
printf '{"tool_name":"Edit","cwd":"/","tool_input":{"file_path":"%s"}}' \
  "$PWD/outside/missing//../../repo/AGENTS.md" |
  <checkout>/.agents/hooks/main-checkout-guard.sh; echo "exit $?"

The branch prints the refusal and exit 2; main prints exit 0. The
same with outside/missing/../../repo/AGENTS.md gives the same pair.

The source's Maintenance checks pass on the tip:
bash tests/verify-agent-config.sh (321 passed),
bash tests/check-work-folder.sh, .agents/hooks/shellcheck.sh repo,
prettier --check '**/*.md', scripts/verify-agent-config.sh, and
git diff --check main. The two new cases in
tests/agent-config/main-checkout-guard.sh fail on main with exit 0.
How each client normalizes a path before writing was not checked, as
their source is outside the repository.

## Why The main-checkout guard let an agent write into a main checkout on its main branch when the path climbed back with `..` through a directory that does not exist, such as `/tmp/x/../../repo/AGENTS.md`, while it refused the same file named directly. A mistaken path join or injected text was enough to change the checkout `AGENTS.md` keeps as it was. Fixes #169. Refusing every path with `..` was weighed and set aside: it would also refuse edits the guard passes today, such as `../sibling/x` from a linked worktree. ## What changed The guard now resolves `.` and `..` in the part of the path that does not exist yet, where every name is a missing directory and so resolves exactly, and a leading `..` climbs from the existing directory as the kernel does. It then looks the repository up where the path really lands, so both paths above are refused. Empty names from a doubled slash are dropped first, so `missing//../..` resolves the same way. Paths that do not land in a guarded checkout pass as before. ## How to verify In a scratch directory, make a repository on `main` and run the guard with a path that climbs back into it: ```bash git init -q -b main repo && git -C repo commit -q --allow-empty -m init mkdir outside printf '{"tool_name":"Edit","cwd":"/","tool_input":{"file_path":"%s"}}' \ "$PWD/outside/missing//../../repo/AGENTS.md" | <checkout>/.agents/hooks/main-checkout-guard.sh; echo "exit $?" ``` The branch prints the refusal and `exit 2`; main prints `exit 0`. The same with `outside/missing/../../repo/AGENTS.md` gives the same pair. The source's Maintenance checks pass on the tip: `bash tests/verify-agent-config.sh` (321 passed), `bash tests/check-work-folder.sh`, `.agents/hooks/shellcheck.sh repo`, `prettier --check '**/*.md'`, `scripts/verify-agent-config.sh`, and `git diff --check main`. The two new cases in `tests/agent-config/main-checkout-guard.sh` fail on main with exit 0. How each client normalizes a path before writing was not checked, as their source is outside the repository.
The guard walked up to the first existing directory and kept every
later name, .. included, as text, so a path climbing back through a
missing directory, such as /tmp/x/../../repo/AGENTS.md, was looked up
outside the checkout it lands in and passed. Resolve each . and .. of
that part, exact since every name before it is a missing directory, and
walk again from the path left.

Fixes #169
claudiomaradonna deleted branch fix/guard-dotdot 2026-10-29 19:52:06 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/agent-foundry!172
No description provided.