Skip to content
Merged
Changes from all commits
Commits
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
55 changes: 55 additions & 0 deletions host-setup/agent-safety/claude/gh-write-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -2329,6 +2329,34 @@ def _names_a_stream(target):
return posixpath.normpath(re.sub(r"^/+", "/", target)).startswith(("/dev/", "/proc/"))


_RESERVED_WORDS = frozenset(
{
"!",
"case",
"coproc",
"do",
"done",
"elif",
"else",
"esac",
"fi",
"for",
"function",
"if",
"in",
"select",
"then",
"time",
"until",
"while",
"{",
"}",
"[[",
"]]",
}
)


def _redirects_stdin(after_done):
"""True if `after_done` binds descriptor 0 to a source that ends, which a `read` drains.

Expand All @@ -2341,6 +2369,11 @@ def _redirects_stdin(after_done):

The last binding is what counts, not the first to qualify. A shell applies redirections in
order and each replaces the last, so `< in.txt < /dev/zero` reads the stream.

The loop's command ends at a reserved word as it does at a separator, so the `< f` in
`if while read l; do sleep 30; done then echo x < f; fi` binds the `echo`.
That includes a closing word such as `}`, since a pipe inside the compound it closes can feed
the loop, so `{ yes | while read l; do sleep 30; done } < f` reads the pipe.
"""
bound = False
i = 0
Expand All @@ -2350,6 +2383,8 @@ def _redirects_stdin(after_done):
# `yes | while read l; do sleep 30; done; cat < f` is fed by the pipe.
if _is_separator(tok):
return bound
if tok in _RESERVED_WORDS:
return bound
fd = ""
# A descriptor carries as its own token, so `2>&1 < f` arrives as five.
# Reading the token before the `<` as a descriptor read the previous redirect's target as one.
Expand Down Expand Up @@ -5332,6 +5367,26 @@ def classify(
"deny",
"a redirect on a later command binds nothing this loop reads",
),
(
"if while read l; do sleep 30; done then echo x < f; fi",
"deny",
"nor does one after a reserved word, which ends the loop's command as a separator does",
),
(
"if true; then while read l; do sleep 30; done else echo x < f; fi",
"deny",
"and an `else` ends it the same way a `then` does",
),
(
"if true; then if true; then while read l; do sleep 30; done fi else echo x < f; fi",
"deny",
"even behind a closing word, which ends it too",
),
(
"{ yes | while read l; do sleep 30; done } < f",
"deny",
"since a redirect after a closing word binds a compound whose pipe can still feed the loop",
),
(
"yes | while read l; do sleep 30; done {fd}< f",
"deny",
Expand Down
Loading