feat(bash): add nano-rlm's git history guard to the bash tool - #2526
feat(bash): add nano-rlm's git history guard to the bash tool#2526samsja wants to merge 2 commits into
Conversation
Port nano-rlm's git block (itself mirroring mini_swe_agent_plus's execute_bash.py) into the shared chat program: split each bash command on &&, ||, ;, | and refuse if any segment invokes git, reusing the same refusal string. Wired as BashHarnessConfig.block_git (default true), --env.agent.harness.block-git false to opt out. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the all-git block with nano-rlm's guard: ordinary git commands are allowed, but broad-history git log options (--all, --branches, ...) are refused, with nano-rlm's refusal string and its GIT_HISTORY_GUARD_PROMPT appended to the system prompt when active. Config flips from block_git to allow_git (default false = guard on). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
|
||
|
|
||
| def find_blocked_command(command: str) -> "str | None": | ||
| for segment in _BASH_SEPARATORS.split(command): |
There was a problem hiding this comment.
🟠 High utils/core.py:219
run_bash(..., allow_git=False) executes git log --all inside command substitutions such as echo "$(git log --all)", so the guard can be bypassed and prohibited broad history is exposed. find_blocked_command only inspects the first argv token of each top-level segment; it must also inspect nested shell commands before allowing execution.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/harnesses/utils/core.py around line 219:
`run_bash(..., allow_git=False)` executes `git log --all` inside command substitutions such as `echo "$(git log --all)"`, so the guard can be bypassed and prohibited broad history is exposed. `find_blocked_command` only inspects the first argv token of each top-level segment; it must also inspect nested shell commands before allowing execution.
| def find_blocked_command(command: str) -> "str | None": | ||
| for segment in _BASH_SEPARATORS.split(command): | ||
| blocked = find_blocked_git_log_option(_split_segment(segment)) | ||
| if blocked is not None: | ||
| return blocked | ||
| return None |
There was a problem hiding this comment.
🟡 Medium utils/core.py:218
find_blocked_command refuses benign commands such as printf '%s\n' 'x; git log --all; y', even though Bash prints the quoted text without executing git log. _BASH_SEPARATORS.split(command) treats separators inside shell quotes as command boundaries; tokenize with a quote-aware shlex lexer before checking segments.
def find_blocked_command(command: str) -> "str | None":
- for segment in _BASH_SEPARATORS.split(command):
- blocked = find_blocked_git_log_option(_split_segment(segment))
+ lexer = shlex.shlex(command, posix=True, punctuation_chars=";&|")
+ lexer.whitespace_split = True
+ segment = []
+ for token in lexer:
+ if token in {";", "|", "&&", "||"}:
+ blocked = find_blocked_git_log_option(segment)
+ if blocked is not None:
+ return blocked
+ segment = []
+ else:
+ segment.append(token)
+ blocked = find_blocked_git_log_option(segment)
+ if blocked is not None:
+ return blocked
- if blocked is not None:
- return blocked
return None🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/harnesses/utils/core.py around lines 218-223:
`find_blocked_command` refuses benign commands such as `printf '%s\n' 'x; git log --all; y'`, even though Bash prints the quoted text without executing `git log`. `_BASH_SEPARATORS.split(command)` treats separators inside shell quotes as command boundaries; tokenize with a quote-aware `shlex` lexer before checking segments.
| if not allow_git: | ||
| blocked = find_blocked_command(command) | ||
| if blocked is not None: | ||
| return GIT_REFUSAL.format(cmd=blocked) | ||
| try: |
There was a problem hiding this comment.
🟠 High utils/core.py:227
A non-string command such as 42 raises TypeError and aborts run_chat_loop when the Git history guard is active, instead of returning the bash-tool error string. The guard runs before run_bash's try block, so _BASH_SEPARATORS.split(command) is not converted into an error response; move the guard inside the try.
- if not allow_git:
- blocked = find_blocked_command(command)
- if blocked is not None:
- return GIT_REFUSAL.format(cmd=blocked)
- try:
+ try:
+ if not allow_git:
+ blocked = find_blocked_command(command)
+ if blocked is not None:
+ return GIT_REFUSAL.format(cmd=blocked)🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/harnesses/utils/core.py around lines 227-231:
A non-string `command` such as `42` raises `TypeError` and aborts `run_chat_loop` when the Git history guard is active, instead of returning the bash-tool error string. The guard runs before `run_bash`'s `try` block, so `_BASH_SEPARATORS.split(command)` is not converted into an error response; move the guard inside the `try`.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c700f1b. Configure here.
| blocked = find_blocked_git_log_option(_split_segment(segment)) | ||
| if blocked is not None: | ||
| return blocked | ||
| return None |
There was a problem hiding this comment.
Quoted pipes hide blocked options
Medium Severity
find_blocked_command splits on | and ; before any quote-aware parse, so a restricted option after a quoted delimiter never reaches find_blocked_git_log_option. A single git log whose format or grep pattern contains | or ; can still run --all and leak extra-branch history.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c700f1b. Configure here.
| GIT_REFUSAL = ( | ||
| "Git history option '{cmd}' is not allowed. Use current-branch history only." | ||
| ) | ||
| _BASH_SEPARATORS = re.compile(r"&&|\|\||;|\|") |
There was a problem hiding this comment.
Newlines bypass the history guard
High Severity
_BASH_SEPARATORS handles &&, ||, ;, and | but not newlines, so a later line that is git log --all stays in the same segment. shlex.split then treats the first token as the earlier command and the guard never fires, while bash -c still runs every line.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c700f1b. Configure here.
|
|
||
| def find_blocked_git_log_option(argv: list) -> "str | None": | ||
| if not argv or not _is_git_binary(argv[0]): | ||
| return None |
There was a problem hiding this comment.
Assignment prefixes evade git detection
Medium Severity
find_blocked_git_log_option requires the git binary at argv[0], so a leading environment assignment makes _is_git_binary fail. The restricted git log still runs under bash -c and can leak extra-branch history.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit c700f1b. Configure here.
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This PR changes the default behavior of the production bash tool by blocking broad git-history access and adds substantial shell-command inspection logic. Unresolved findings identify multiple ways the guard can be bypassed or mishandle commands, so the enforcement behavior requires human review. Not approved because:
Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more. |


Overview
The bash+edit harness had no git guard at all:
run_bashexecuted commands raw, so agents could read gold patches straight out of repo history withgit log --all. This ports nano-rlm's current git history guard, with full behavioral parity.Details
status,diff,commit, current-branchgit log) stays available; only broad-historygit logoptions are refused:--all,-all,--branches[=..],--remotes[=..],--tags[=..],--glob[=..],--alternate-refs,--reflog,--walk-reflogs,-g. Handles chained commands (split on&&/||/;/|), git global options (-C,-c, ...), path-prefixed binaries (/usr/bin/git), and the--pathspec terminator — same as nano-rlm'sgit_block.py.GIT_HISTORY_GUARD_PROMPTis appended verbatim to the system prompt while the guard is active, so the model is told up front (see fix: include git history guard prompt when the bash tool is active nano-rlm#172 which closes the same prompt gap for nano-rlm's own bash tool).Git history option '--all' is not allowed. Use current-branch history only.BashHarnessConfig.allow_git(defaultfalse= guard on, mirroring nano-rlm's policy naming);--env.agent.harness.allow-git truelifts the guard and drops the prompt line.Verified with a throwaway script: a 26-command corpus asserted identical outputs between this predicate and nano-rlm's
find_blocked_command, plus byte-equality of the refusal template and guard prompt against the nano-rlm source, argv wiring, and both bundled program variants carrying the guard.ruff check/formatpass.🤖 Generated with Claude Code
Note
Medium Risk
Changes default bash tool behavior for all bash harness rollouts (commands may be refused that previously ran); opt-in
allow_gitrestores prior unrestricted history access.Overview
The bash harness enables a git history guard by default so eval agents cannot mine solutions from broad
git loghistory (e.g.git log --all), matching nano-rlm behavior.Runtime enforcement in the bundled chat program inspects bash commands (including chained segments) and refuses
git logwith broad-history flags (--all,--branches,--remotes,--tags,--glob, reflog options, etc.) while leaving normal git and current-branchgit logalone. Blocked calls return a fixed refusal message instead of running the shell.Harness wiring: new
BashHarnessConfig.allow_git(defaultfalse) appends nano-rlm’sGIT_HISTORY_GUARD_PROMPTto the system prompt when the guard is on and passes--allow-gitonly when operators opt in via--env.agent.harness.allow-git true.Reviewed by Cursor Bugbot for commit c700f1b. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Add git history guard to
BashHarnessto block broadgit logaccessgit loginvocations using broad-history options (e.g.--all, branch globs, remotes, tags) whenallow_gitis false&&,||,;, and pipes so each is checked independently; falls back to whitespace splitting when shell parsing failsBashHarnessConfigdefaultsallow_gitto false, appending a system-prompt warning about restricted git history; enabling it omits the warning and passes--allow-gitto the child chat programrun_bashin core.py defaultsallow_gitto true for direct callers, but the bash harness sets it to false by default — broadgit logcommands now return a refusal message instead of executing📊 Macroscope summarized c700f1b. 2 files reviewed, 3 issues evaluated, 0 issues filtered, 3 comments posted
🗂️ Filtered Issues