Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
2480163
feat(daemon): guard cross-worktree Git mutations
wenshao Aug 7, 2026
2b0e8cd
fix(daemon): keep Git guard off serve fast path
wenshao Aug 7, 2026
107b5f6
Merge branch 'main' into feat/daemon-git-worktree-guard
wenshao Aug 7, 2026
b175cd7
fix(serve): close daemon Git guard parser bypasses
wenshao Aug 7, 2026
2af7419
Merge branch 'main' into feat/daemon-git-worktree-guard
qwen-code-dev-bot Aug 7, 2026
b1b7606
fix(serve): keep daemon Git guard out of serve fast-path closure
wenshao Aug 7, 2026
83428be
fix(serve): close re-reviewed daemon Git guard bypasses and subagent …
wenshao Aug 8, 2026
18e997a
fix(serve): restore lazy daemon Git guard import
wenshao Aug 8, 2026
ab1e3e2
fix(serve): close daemon Git guard shell front-end bypasses
wenshao Aug 8, 2026
45753e0
fix(serve): restore the shell-wrapper analysis return shape
wenshao Aug 8, 2026
c14ffc4
fix(serve): close the remaining daemon Git guard front-end gaps
wenshao Aug 8, 2026
7f66be6
fix(serve): resolve the repository Git discovers, not just the directory
wenshao Aug 8, 2026
b11585b
fix(serve): match chdir, brace-expansion and here-string semantics
wenshao Aug 8, 2026
dfeb461
fix(serve): treat relocated git describe as a target-repo write
wenshao Aug 8, 2026
af36eca
docs(serve): state what git describe actually rewrites
wenshao Aug 8, 2026
8112fae
test(serve): cover the provider-attached marker with a real handshake
wenshao Aug 8, 2026
338e0c7
fix(serve): close the round-3 shell and repository-discovery gaps
wenshao Aug 8, 2026
d9a3911
fix(serve): scope the relink invalidation to path-resolving Git runs
wenshao Aug 8, 2026
012d252
fix(serve): rebuild the relink defense and close the round-4 gaps
wenshao Aug 9, 2026
2bf64cd
fix(serve): carry relink and shell state across nested scopes
wenshao Aug 9, 2026
0a58fe7
fix(daemon): evaluate the guard against the directory the tool runs in
wenshao Aug 9, 2026
cbcde28
Merge branch 'main' into feat/daemon-git-worktree-guard
wenshao Aug 9, 2026
3a247f7
fix(daemon): reach the real guard path and close the round-6 escapes
wenshao Aug 10, 2026
a1652cc
fix(daemon): contain a sub-agent to an in-project agent worktree
wenshao Aug 10, 2026
581d2bb
test(acp): assert the session identity and cwd the guard now receives
wenshao Aug 10, 2026
6b755d1
fix(daemon): record ordinary targets a dynamic relinker re-points
wenshao Aug 10, 2026
8baf7a7
Merge branch 'main' into feat/daemon-git-worktree-guard
qwen-code-dev-bot Aug 10, 2026
0f970d4
fix(daemon): close the round-7 escapes, verified with the reported pa…
wenshao Aug 10, 2026
8e46dba
fix(daemon): repair the round-7 patch and bound what this guard promises
wenshao Aug 11, 2026
089c3e3
fix(daemon): close the round-9 critical forms an agent may actually emit
wenshao Aug 11, 2026
a2736b8
Merge remote-tracking branch 'origin/main' into work8687
wenshao Aug 11, 2026
cf2ea23
fix(daemon): replay an alias with the args its invocation appends
wenshao Aug 11, 2026
7f4746f
fix(daemon): carry a function/alias body's cwd and exports to the caller
wenshao Aug 11, 2026
6273464
fix(daemon): inherit the caller's allexport into a same-shell body
wenshao Aug 11, 2026
01587ab
fix(daemon): complete the same-shell state model for bodies and subst…
wenshao Aug 11, 2026
2f0b99f
fix(daemon): resolve shadowing and exported functions; isolate pipe s…
wenshao Aug 11, 2026
a2ae8bb
fix(daemon): close the interlocking gaps in my function-model work
wenshao Aug 11, 2026
6deb8bd
fix(daemon): deny relocations disguised by a redirection
wenshao Aug 11, 2026
a43faa8
fix(daemon): deny relocation hidden by a leading redirect or a backgr…
wenshao Aug 12, 2026
1203072
Merge remote-tracking branch 'origin/main' into work8687
wenshao Aug 12, 2026
e595f7b
fix(daemon): don't let a harmless or removed shadow mask a relocation
wenshao Aug 12, 2026
70f5162
fix(daemon): drop exported functions when env clears the child enviro…
wenshao Aug 12, 2026
68eb534
test(daemon): pin the sh-wrapper fail-closed contract and document it
wenshao Aug 12, 2026
3333331
fix(daemon): model shell-definition removal the way the real shell does
wenshao Aug 12, 2026
636602c
fix(daemon): bare unset keeps the function and env -u strips exported…
wenshao Aug 12, 2026
5fc219d
fix(daemon): fail closed when a removal builtin could retract a track…
wenshao Aug 13, 2026
6128e69
fix(daemon): skip leading redirections before the removal-builtin pre…
wenshao Aug 13, 2026
d95d87c
fix(daemon): replay a shadowed removal builtin and drop unset variables
wenshao Aug 13, 2026
a5f8099
fix(daemon): honor shadowed command/builtin prefixes and PATH-based r…
wenshao Aug 13, 2026
c7b6aa4
fix(daemon): catch delimiter-glued relocations and redirect-decoy pre…
wenshao Aug 14, 2026
a0427ae
fix(daemon): deny trailer/man/sendemail command-executing config keys
wenshao Aug 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions docs/design/daemon-git-worktree-guard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Daemon Git worktree guard

## Context

A daemon ACP session is owned by one bound workspace. The model shell tool
already rejects an explicit `directory` outside its effective workspace, but a
Git command can relocate itself with `-C`, `--work-tree`, or `--git-dir` while
the shell process still starts inside the workspace. This can let a daemon
agent mutate another checkout or worktree after the direct directory form was
rejected.

## Scope

The guard applies only to model tool execution through the managed daemon ACP
path. It does not change CLI or TUI shell validation, Git safety classification,
permission rules, confirmation behavior, or direct user shell execution.

The daemon enables its managed tool guard for every ACP child. The host owns the
bound workspace and adds it to the validated guard request before applying the
built-in policy. An optional external tool guard remains an additional policy
and receives the same request only after the built-in policy allows it.

## Policy

The built-in guard inspects `run_shell_command` calls only. Command splitting
reuses core `splitCommands`; containment reuses core `realpathNearestExisting`
and `isWithinRoot`. It recognizes Git invocations whose repository location is
changed by literal forms of:

- `git -C <path>` and `git -C<path>`
- `git --work-tree <path>` and `git --work-tree=<path>`
- `git --git-dir <path>` and `git --git-dir=<path>`
- leading `GIT_DIR`, `GIT_WORK_TREE`, `GIT_COMMON_DIR`, or `GIT_INDEX_FILE`
assignments
- directory-shifting wrapper flags `env -C`/`--chdir` and `sudo -D`/`--chdir`
- `cd`, `pushd`, or `popd` builtins earlier in the same command chain, whose
targets become the containment basis for later Git invocations in that chain

Wrapper prefixes are unwrapped before Git detection: leading env assignments,
`command`, `env` (with its value-taking flags), `sudo` (with its value-taking
flags), `nohup`, `exec`, `timeout <duration>`, `sh|bash|dash|zsh|ksh -c`
payloads (analyzed recursively), `eval` payloads (analyzed recursively, with
cwd changes propagated because `eval` runs in the current shell),
path-qualified Git binaries by basename, and leading `{`/`!` shell syntax.
A segment whose program token cannot be classified (shell expansions) fails
closed when the segment also carries a Git relocation marker or a recorded
relocation.

Relative targets resolve from the command's effective starting directory:
`arguments.directory` when present, otherwise the session's current effective
working directory. The bridge supplies both that current directory and the
immutable bound workspace from trusted session state. The current effective
working directory is the allowed execution boundary so a session moved through
the controlled daemon `/cd` flow can operate in its selected worktree without
being mistaken for an escape from the original storage owner. Git applies `-C`
during option parsing and resolves relative `--git-dir`/`--work-tree` against
the post-`-C` cwd, so relative targets resolve against the final cwd of the
`-C` chain regardless of argv order.

A statically resolved Git relocation is denied when both of the following
hold:

1. its target is outside the session's effective working directory after
canonical path resolution;
2. its Git subcommand is mutating or cannot be classified as read-only.

Relocated commands whose subcommand is in a small verified read-only set
(`status`, `rev-parse`, `ls-files`, `grep`, `describe`, `cat-file`) remain
allowed. `diff`, `log`, `show`, and `blame` are excluded from that set:
`--output` writes files, and textconv-style drivers execute programs
configured by the target repository. Any `--output` flag demotes an
invocation. Commands with no recognized relocation retain existing behavior.
Dynamic relocation targets (`$` expansions, backticks, leading `~`, globs)
and command-executing `-c`/`--config-env` assignments (`alias.*`,
`core.editor`, `core.pager`, `credential.helper`, `filter.*`, `difftool.*`,
`mergetool.*`, `core.fsmonitor`, or values starting with `!`) are denied for
mutating or unknown subcommands because the daemon cannot prove that the
target remains inside the effective working directory.

`--git-dir` is evaluated by the repository git operates on, with
canonicalization before basename handling: a target whose canonical form ends
in `.git` uses its parent; a `.git` gitfile is followed through its `gitdir:`
redirect; a per-worktree administrative directory
(`<repo>/.git/worktrees/<name>`) is resolved through its `gitdir` file to the
linked worktree checkout. Unresolvable indirections fail closed.

## Failure semantics

Malformed managed guard requests, stale session or prompt ownership, missing
trusted workspace context, policy exceptions, and malformed external-provider
responses fail closed before execution. Unparseable commands, dangling
relocation options, relocation targets that do not fully exist at decision
time (a missing target can still become an outward symlink before git runs),
and unreadable Git indirections are denied for mutating or unclassifiable
subcommands. A built-in denial is final and is not sent to the optional
provider. Denial reasons are length-clamped and control-character-stripped so
they always satisfy the guard result validation.

The managed guard plumbing is active for every daemon ACP child because the
built-in policy needs it. The child-side v1 restrictions (`/fork` and
agent-backed workspace memory remember/dream) key on the external provider
being attached, not on the plumbing's mere presence: under the built-in guard
alone, hidden-agent tool calls traverse the same managed guard and are
inspected by the same daemon-side policy. Without a provider the child also
resolves every non-shell tool call locally (the built-in policy allows them
structurally) instead of paying a child-daemon-child round trip per call;
`run_shell_command` always makes the round trip. With a provider attached
every call still makes it.

## Non-goals

- No changes to core `ShellTool`, `ShellToolInvocation`, shell AST parsing,
`PermissionManager`, `evaluatePermissionFlow`, or `CoreToolScheduler`.
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
Comment thread
wenshao marked this conversation as resolved.
Resolved
- No new confirmation flow or linked-worktree exception.
- No restriction on direct user-entered daemon shell commands.
- No general shell interpreter or environment-variable analysis: script files
run by `bash script.sh` or `source` are not read, and variable values are
not tracked across commands.
- No heredoc body analysis: Git-shaped text inside a heredoc is scanned as
executable lines and can be denied even though the shell never executes it
(a fail-closed false positive, not a bypass).
Comment thread
wenshao marked this conversation as resolved.
Resolved
- No attempt to correlate a denial with a previous tool call.
Loading
Loading