Skip to content
Merged
Show file tree
Hide file tree
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
13 changes: 8 additions & 5 deletions host-setup/agent-safety/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,14 @@ text says, because the harm it covers was never in the text.
text in a `$(...)` substitution. It is also one ending a quoted heredoc's data line, unless the
delimiter line follows. It is not one inside `$'...'`, or single quotes within double quotes, a
backquote, or `${...}`. A Markdown hard line break in a quoted heredoc or a single-quoted body
is one. The single-quoted part must be its word's only quoted part. Where the text names git or
gh, it must also hold no further quoting. Each backslash-newline in it must then sit in its
command text or end a comment there. The command is then read again as a shell running that text reads it,
joining the lines except where one ends a comment. It is also read with each one joined and with
each one a line break, and denied where any reading is. Nor does either read a single unquoted
is one. The single-quoted part must be its word's only quoted, escaped, or expanded part, with no
escaped word break just before it. Where the text names git, gh, or a push, it must also hold no
further quoting. Those names are read in the text as written, with its lines joined, and with
each one a line break. Each backslash-newline in that text must then sit in its command text or
end a comment there. The command is then read again as a shell running that text reads it,
joining the lines except where one ends a comment. It is also read with each one joined and with each one a line break, and denied where
any reading is. Every reading keeps each escaped backslash pair before a line break, which the
shell reads as one literal backslash. Nor does either read a single unquoted
backslash-newline between a command separator such as `&&` or `|` and a non-operator character,
which splits no word. Two comments whose
apostrophes pair up as one quoted span are read as the shell reads them, as comments. The
Expand Down
59 changes: 50 additions & 9 deletions host-setup/agent-safety/claude/gh-write-guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4239,12 +4239,13 @@ def command(i, closer):


def _literal_span_kept(cmd, start, stop, word, inner):
"""True where a shell running the `literal` text `cmd[start:stop]` reads each backslash-newline in it as `inner` places it.
"""True where the readings of the `literal` text `cmd[start:stop]` show what a shell running it runs.

A span in a word with any other quoted or expanded part is never kept, since the shell runs the whole word.
A span naming no git or gh, however its backslash-newlines are read, is kept, since its readings show what it runs.
A span in a word with any other quoted, escaped, or expanded part is never kept, since the shell runs the whole word.
Nor is one with an escaped word break just before it.
A span naming no git, gh, or push, as written, joined, or with line breaks, is kept whatever its own scan found.
Any other span is kept only where its own scan finished and it holds no quoting a further shell would unwrap.
Each backslash-newline in it must then sit in command text or end a comment there.
Each backslash-newline in it must then sit in command text or end a comment there, as `inner` places it.
"""
if word:
head = cmd.rfind(" ", 0, start - 1) + 1
Expand Down Expand Up @@ -4280,7 +4281,8 @@ def _line_break_reading(cmd):
One reading takes each kept backslash-newline as the span's own scan places it.
A shell running the span reads one ending a comment there as a line break, and joins any other.
The other readings join each one, or read each one as a line break.
An escaped backslash before a kept newline is a line break in all of them.
Each escaped backslash pair before a kept newline stays in all of them, read as one literal backslash.
A space parts those pairs from each line break a reading puts after them, so that break is not read again.
A scan that does not finish leaves every backslash-newline in a context the shell joins.
"""
try:
Expand All @@ -4304,9 +4306,13 @@ def _line_break_reading(cmd):
spans[start, stop] = (inner or {}) if kept else None
inner = spans[start, stop]
if inner is not None:
joined = cmd[m.start() : nl - 1] if odd else "\n"
pairs = cmd[m.start() : nl - 1 if odd else nl]
broken = f"{pairs} \n" if pairs else "\n"
joined = pairs if odd else broken
ends_comment = inner.get(nl - start) == "comment"
edits.append((m.start(), nl + 1, joined, "\n" if ends_comment else joined))
edits.append(
(m.start(), nl + 1, joined, broken if ends_comment else joined, broken)
)
continue
kind = None
if not odd:
Expand All @@ -4329,10 +4335,10 @@ def _line_break_reading(cmd):
if not edits:
return False, ()
readings = []
for pick in (3, 2, None):
for pick in (3, 2, 4):
out, at = [], 0
for edit in edits:
out.append(cmd[at : edit[0]] + ("\n" if pick is None else edit[pick]))
out.append(cmd[at : edit[0]] + edit[pick])
at = edit[1]
readings.append("".join(out) + cmd[at:])
return False, tuple(dict.fromkeys(readings))
Expand Down Expand Up @@ -9325,6 +9331,41 @@ def _nested_wrapper(body, depth):
"deny",
"an escaped backslash before a newline in a single-quoted body is a line break to the inner shell",
),
(
"bash -c 'git push origin main\\\\\necho hi'",
"allow",
"an escaped backslash pair before a line break in a shell body stays a literal backslash on the word",
),
(
"bash <<'EOF'\ngit push origin main\\\\\necho hi\nEOF",
"allow",
"a quoted heredoc fed to a shell keeps the literal backslash an escaped pair leaves before a line break",
),
(
"bash -c 'git push origin HEAD:main\\\\\\\n'",
"allow",
"an escaped pair before a backslash-newline in a shell body stays a literal backslash in every reading",
),
(
"bash -c 'gh\\\\\npr merge 5 --admin'",
"allow",
"an escaped backslash pair after gh in a shell body names a command other than gh",
),
(
"bash -c 'git push origin feature/x\\\\\ngit push origin main'",
"deny",
"control: the line after an escaped backslash pair in a shell body is still read",
),
(
"bash -c 'true # x\\\\\ngit push origin main'",
"deny",
"control: an escaped backslash pair ending a comment in a shell body leaves the next line read",
),
(
"bash -c 'true # x\\\\\\\ngit push origin main'",
"deny",
"control: an escaped pair before a comment-ending backslash-newline leaves the next line read",
),
(
"bash -c 'true # x\\\ngh pr merge 5 --ad\\\nmin'",
"deny",
Expand Down
Loading