Skip to content

fix(approval): hardline in-place edits of Hermes config/env - #60523

Open
qni-live wants to merge 1 commit into
NousResearch:mainfrom
qni-live:fix/hardline-hermes-config-inplace-edit
Open

fix(approval): hardline in-place edits of Hermes config/env#60523
qni-live wants to merge 1 commit into
NousResearch:mainfrom
qni-live:fix/hardline-hermes-config-inplace-edit

Conversation

@qni-live

@qni-live qni-live commented Jul 7, 2026

Copy link
Copy Markdown

What

Adds three patterns to HARDLINE_PATTERNS in tools/approval.py that unconditionally block in-place edits of ~/.hermes/config.yaml and ~/.hermes/.env via sed -i, sed --in-place, and perl/ruby -i. They mirror the identical regexes that already exist in DANGEROUS_PATTERNS, reusing the existing _HERMES_CONFIG_PATH / _HERMES_ENV_PATH path fragments (so $HOME, ${HOME}, and $HERMES_HOME spellings are covered too).

Why

With approvals.mode: smart, an aux LLM adjudicates commands that match DANGEROUS_PATTERNS. Observed in the field (Windows install, July 2026): the adjudicator approved sed -i 's/.../.../' ~/.hermes/config.yaml even though the matched rule's description ("in-place edit of Hermes config/env") was correctly surfaced to it.

config.yaml is the security policy — approvals.mode, yolo, and the permanent-approval allowlist live there, and the mtime-keyed config cache means a write takes effect mid-session. .env holds credentials and is already deny-listed on the file_tools side (write_file/patch refuse it). One generous smart verdict on the terminal side therefore lets the agent rewrite its own guardrails, making the file-tool deny unpaired theater. That fits the hardline bar ("a floor below yolo"): the blast radius is every other guard.

Scope / non-goals

  • Reads (cat, grep) and sed without -i on these files are unaffected.
  • Project-local config.yaml / .env files keep their existing (smart-approvable) treatment via _PROJECT_CONFIG_PATH / _PROJECT_ENV_PATH.
  • Other write vectors (tee, >, cp) already have their own rules; this PR only promotes the in-place-editor family that was observed bypassing smart approval.

Tests

  • 9 new block cases and 5 new allow cases in tests/tools/test_hardline_blocklist.py (flag spellings -i, -i.bak, -ri, --in-place, macOS -i '', perl -pi/-i -pe, ruby -i -pe; path spellings ~, $HOME, ${HOME}, $HERMES_HOME).
  • HARDLINE_PATTERNS grows 12 → 15, under the test_hardline_list_is_small cap of 20; the compile-cost comment in approval.py is updated to match.
  • pytest tests/tools/test_hardline_blocklist.py tests/tools/test_approval.py tests/tools/test_shell_bypass_denylist.py: 535 passed.

🤖 Generated with Claude Code

@alt-glitch alt-glitch added type/security Security vulnerability or hardening comp/tools Tool registry, model_tools, toolsets area/auth Authentication, OAuth, credential pools sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data P2 Medium — degraded but workaround exists labels Jul 7, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The new hardline entries still match these in-place-edit strings when they are quoted data inside another command, so normal repository operations such as git commit -m "sed -i s/a/b/ ~/.hermes/config.yaml" or echo "perl -pi -e s/a/b/ ~/.hermes/.env" become unconditional hardline denials. Hardline blocks cannot be bypassed even with yolo, approvals.mode=off, or cron approval mode, so this is a stronger false positive than the existing smart-approvable dangerous-pattern match. The existing catastrophic hardline rules use command-position tokenization to avoid this quoted-prose case, but the new sed/perl/ruby hardline patterns are bare searches over the whole command.

Security evidence:

  • trust boundary: terminal approval guard decides which local commands are impossible for the agent to run, regardless of approval mode.
  • source/sink/invariant: only actual in-place editor commands targeting Hermes config/env should hit the hardline floor; quoted prose in arguments to unrelated commands must remain executable.
  • current-main reproduction: current main keeps these patterns in DANGEROUS_PATTERNS, so the same quoted-data false positive is still approvable rather than an unconditional hardline denial.
  • PR-head or patch-replay validation: on a patch replay against current main c30c9753b6efc08e154d66b6501a444739df3859, importing tools.approval from the replay worktree, check_all_command_guards('git commit -m "sed -i s/a/b/ ~/.hermes/config.yaml"', "local") returned approved=False, hardline=True; the same happened for echo "perl -pi -e s/a/b/ ~/.hermes/.env".
  • positive/negative cases: the actual sed -i s/a/b/ ~/.hermes/config.yaml command hardline-blocked as intended, and the focused approval tests passed locally as 535 passed.
  • residual bypass search: the new regexes are not anchored to command positions and the new regression tests do not cover sed/perl/ruby strings carried as quoted data.
  • reviewer validation: CodeRabbit completed with zero findings; local full-mode review reproduced the blocker on the replay worktree.

Signed: GPT-5.5-xhigh in Codex

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing a real approval-boundary gap. Current main still classifies in-place edits of Hermes config/env as dangerous (tools/approval.py:733-743) and sends smart-mode findings to _smart_approve() (tools/approval.py:2838-2862), so promoting actual editor commands to the hardline floor is directionally sound.

Problems

  • tools/approval.py:402-404 uses unanchored searches across the entire command. The exact added regex matches quoted data such as git commit -m "sed -i s/a/b/ ~/.hermes/config.yaml" and echo "perl -pi -e s/a/b/ ~/.hermes/.env"; hardline responses are unconditional (tools/approval.py:561-572). This regresses the existing quoted-data contract in tests/tools/test_hardline_blocklist.py:242-274, added by 7534b5be2.

Suggested changes

  • Anchor each new rule at a real command position via _CMDPOS, with the existing quote-aware _command_detection_variants() support (tools/approval.py:1401-1431).
  • Add quoted-data negative regressions for the new sed/perl/ruby patterns.

Automated hermes-sweeper review.

Comment thread tools/approval.py Outdated
@@ -399,11 +399,14 @@ def _hardline_rm_path(path_alt: str, tail: str = r'(?:\s|$|[)`;|&])') -> str:
(_CMDPOS + r'init\s+[06]\b', "init 0/6 (shutdown/reboot)"),
(_CMDPOS + r'systemctl\s+(poweroff|reboot|halt|kexec)\b', "systemctl poweroff/reboot"),
(_CMDPOS + r'telinit\s+[06]\b', "telinit 0/6 (shutdown/reboot)"),
(rf'\bsed\s+-[^\s]*i.*(?:{_HERMES_CONFIG_PATH}|{_HERMES_ENV_PATH})', "in-place edit of Hermes config/env (hardline)"),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole-command search also matches editor syntax inside quoted data, e.g. git commit -m "sed -i s/a/b/ ~/.hermes/config.yaml", making that unrelated command unconditionally hardline-blocked. Please anchor this and the sibling editor rules at an actual command position (the existing _CMDPOS plus quote-aware command variants provides that contract).

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 15, 2026
@qni-live
qni-live force-pushed the fix/hardline-hermes-config-inplace-edit branch from 16ad3ac to 9b7bd72 Compare July 19, 2026 21:06
@qni-live

Copy link
Copy Markdown
Author

Thanks @egilewski and @teknium1 — you were both right about the bare searches. Reworked in 9b7bd72:

  • All three patterns are now anchored to _CMDPOS, so in-place-edit spellings appearing as quoted data (git commit -m "sed -i … config.yaml", echo "perl -pi …") no longer trip the unconditional floor. Both examples from the review are covered as allow-side regression tests.
  • I also rebased onto current main, which matters beyond hygiene: _command_detection_variants() there surfaces every shell -c payload as its own detection variant (payload command at start-of-string), so bash -c 'sed -i … config.yaml' still lands on the hardline floor with the anchored patterns. On the old base that machinery didn't exist yet, and anchoring alone would have reopened exactly the bypass this PR targets — the smart adjudicator had already approved a config-editing sed once, and shell command via -c/-lc flag is a plausible user-allowlist entry.
  • Regression tests cover both directions: quoted-data allow cases, and block cases at every real command position (&&, ;, $( ), subshell, sudo/env wrappers, pipe, bash -c/sh -c payloads).

One known limit, intentionally out of scope: indirect path delivery (echo ~/.hermes/config.yaml | xargs sed -i …, find ~/.hermes … -exec sed -i) doesn't reach the floor. That's consistent with the existing xargs rm / find -exec rm rules, which live in DANGEROUS_PATTERNS — a regex floor can't chase data flow without unacceptable false positives. Happy to follow up with DANGEROUS-level coverage for those forms in a separate PR if you'd like.

@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

The _CMDPOS anchoring fixes the quoted-data examples from the previous review, but it also leaves ordinary spellings of the same protected-file mutation outside the hardline floor. In tools/approval.py:365-375,467-468, the command anchor recognizes only a limited wrapper set and the sed rule requires literal sed with an i in its first option token. These all reached approved=true with HERMES_YOLO_MODE=1 on a replay against current main:

sed -e 's/a/b/' -i ~/.hermes/config.yaml
command sed -i 's/a/b/' ~/.hermes/config.yaml
env -i sed -i 's/a/b/' ~/.hermes/config.yaml
/usr/bin/sed -i 's/a/b/' ~/.hermes/config.yaml

The first form is valid GNU sed and performed the in-place edit in a focused probe. Please detect the actual command word and parse the sed option sequence rather than requiring one narrow spelling, then add regressions for these forms.

There is also a remaining unconditional false-positive class because the protected path is searched anywhere after the sed options rather than as the target operand. Both sed -i 's|~/.hermes/config.yaml|config.yml|' README.md and sed -i 's/a/b/' ~/.hermes/config.yaml.bak hardline-blocked on the replay, although neither mutates the approval-policy file. Please constrain the protected path to a target argument with an exact filename boundary and cover path text inside the sed program plus backup-suffix targets.

Security evidence:

  • trust boundary: check_all_command_guards() decides whether terminal commands remain impossible under yolo or disabled approvals; these hardline rules are the unconditional floor protecting Hermes configuration and environment files.
  • source/sink/invariant: actual sed/perl/ruby in-place mutations of the protected files must be rejected, while commands targeting other files and merely carrying protected-path text must not be hardline-rejected.
  • current-main reproduction: current main has no promoted in-place-editor hardline rules, while the prior PR head reproduced both quoted-data false positives from the earlier review.
  • PR-head or patch-replay validation: the current PR head and its clean replay onto current main fixed the original quoted-data cases but yolo-approved all four equivalent mutation forms and hardline-rejected both non-target examples above.
  • positive/negative cases: intended bare editor forms, chained commands, and shell -c payloads blocked; quoted outer-command strings passed; four equivalent in-place variants bypassed; two non-target mutations false-positive blocked.
  • residual bypass search: covered sed option reordering, command and env wrappers, absolute executable paths, shell payload extraction, protected-path text inside a sed program, and filename suffixes; indirect xargs/find dataflow remains outside this PR's stated scope.
  • reviewer validation: the focused hardline suite passed 213 tests; the combined approval suites passed 562 tests with one unrelated failure that reproduced unchanged on current main.

Signed: GPT-5.6-sol-xhigh in Codex

@qni-live
qni-live force-pushed the fix/hardline-hermes-config-inplace-edit branch from 9b7bd72 to 66b596e Compare August 15, 2026 17:41
@qni-live

Copy link
Copy Markdown
Author

Thanks — both findings reproduced exactly as described, and both are now fixed by
replacing the regexes with an option-grammar parser. Force-pushed a single commit
rebased onto current main.

What was wrong

The regex approach was wrong in both directions, and no amount of pattern
tightening fixes that: matching on command text cannot distinguish the command
word from an argument, or a target operand from program text.

Bypasses. The pattern required literal sed with an i in its first option
token. All four of your cases reproduced here under HERMES_YOLO_MODE=1:

sed -e 's/a/b/' -i ~/.hermes/config.yaml
command sed -i 's/a/b/' ~/.hermes/config.yaml
env -i sed -i 's/a/b/' ~/.hermes/config.yaml
/usr/bin/sed -i 's/a/b/' ~/.hermes/config.yaml

False positives. The protected path was searched anywhere after the options
rather than as a target operand. Both of your cases reproduced:

sed -i 's|~/.hermes/config.yaml|config.yml|' README.md
sed -i 's/a/b/' ~/.hermes/config.yaml.bak

What replaces it

_detect_hermes_inplace_edit() in tools/approval.py, consulted by
detect_hardline_command() alongside HARDLINE_PATTERNS:

  1. Resolve the real command word. Step over sudo / env / command /
    nohup / setsid / nice and friends, including their own options that take
    an argument (env -u PATH, sudo -u root) and VAR=VAL assignments, then
    take the basename so /usr/bin/sed resolves to sed.
  2. Walk the option sequence. Decide whether in-place mode is genuinely on,
    wherever the flag sits — -i, -ri, -pi, -i.bak, --in-place=.bak, or
    after an earlier -e. Short-option bundles are decoded, and options that
    consume the following token are accounted for so their argument is not
    mistaken for a file.
  3. Compare file operands, not text. Without an explicit -e/-f the first
    operand is the program text rather than a file — that is what keeps the
    README.md case out. The protected paths are matched as whole tokens with an
    exact filename, so config.yaml.bak and config.yaml.orig stay out for the
    same reason _WRITE_TARGET_BOUNDARY already keeps .env#backup out of the
    redirection deny.

Option parsing deliberately runs on original-case text: perl/ruby -I is an
include directory that takes an argument and must not be read as -i.

Two additions beyond the reported findings, both flagged here for scoping:

  • The spelled-out home directory (/home/u/.hermes/config.yaml) is now covered.
    The tilde/$HOME-only patterns missed it while naming the same file. Happy to
    drop this if you would rather keep the diff to the reported cases.
  • HARDLINE_PATTERNS gains no new entries — the three regexes are gone and the
    check runs as a function, so the list stays at 12.

Indirect path delivery via xargs, find -exec, or variable expansion remains
at the DANGEROUS/smart level, unchanged and as previously scoped — those never
name the target at a statically resolvable command position.

Validation

  • Trust boundary. check_all_command_guards() decides whether a terminal
    command stays impossible under yolo or disabled approvals; these hardline rules
    are the unconditional floor over the Hermes approval policy and credential
    files.
  • Invariant. Real in-place mutations of the protected files are rejected;
    commands targeting other files, or merely carrying protected-path text, are not.
  • Your six cases. All six reproduced on the previous PR head. All six are
    correct on this head — the four bypasses now block, the two false positives now
    pass.
  • Regressions added. 12 new block cases (your four, plus flag-after-options,
    long form with attached suffix, wrapper options taking an argument, --
    end-of-options, -f-supplied script, perl -i.bak, protected file as a later
    operand, absolute home path) and 8 new allow cases (your two, plus
    .orig/~ suffixes, path inside the sed program via -e, ruby -I and
    perl -I, and reading the file with diff).
  • Suites. tests/tools/test_hardline_blocklist.py passes in full, including
    test_yolo_env_var_cannot_bypass_hardline, which replays every block case with
    HERMES_YOLO_MODE=1. Combined approval suites: 412 passed, 2 failed. Both
    failures are TestDetectDangerousRm in tests/tools/test_approval.py, are
    environmental (OSError: [WinError 1314] — this Windows host lacks the symlink
    privilege), and reproduce unchanged with the commit reverted.
  • Residual bypass search. Covered option reordering and bundling, the
    command/env/sudo/nohup wrapper set including their argument-taking
    options, absolute executable paths, shell -c payload extraction, protected
    path text inside the sed program, backup and derived filename suffixes, and the
    -I versus -i case collision. xargs/find dataflow remains out of scope.

@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

fix(approval): hardline in-place edits of Hermes config/env

The grammar-aware approach (resolve the real command word, walk the editor's option grammar, compare FILE operands as whole tokens) is a clear improvement over text-regex matching, and the test matrix is impressive. Observations, mostly about the boundary of what the grammar check can see:

  1. tools/approval.py _is_protected_hermes_operand — no path normalization beyond backslash→slash and lowercasing. Trivial alternations of the same protected file bypass the whole-token match: ~/.hermes/./config.yaml, ~/.hermes//config.yaml, ~/.hermes/config.yaml/, and (on case-sensitive shells the lowercasing already covers it) .env vs .Env is handled. Since this is the unconditional floor — the one guard the smart-approval adjudicator cannot bypass — normalizing ./.. segments and collapsing duplicate slashes (via os.path.normpath on the token after expanding ~/$HOME) would close a trivial bypass class. At minimum document that these spellings are out of scope.
  2. Relative-path bypass: cd ~/.hermes && sed -i 's/a/b/' config.yaml — the variant splitter surfaces sed -i 's/a/b/' config.yaml as its own variant (per the bash -c test comment), and the operand config.yaml is not protected because the prefix is gone. The cd-relative form mutates the exact same file. This is a genuine gap for a hardline floor; consider tracking a "current directory hint" from a preceding cd in the same command, or explicitly document that cd-relative edits are handled by the file_tools-side protection instead.
  3. Glob bypass: sed -i 's/a/b/' ~/.hermes/*.yaml — the literal * remains in the token, so the basename check fails. Shell expansion turns this into the protected file. Same class as Support passing morph snapshot id #2; a comment acknowledging glob-based delivery is out of scope would set expectations.
  4. Wrapper-option parsing: _resolve_command_word treats any -x token after a wrapper as an option unless it's in _WRAPPER_OPTS_WITH_ARG. For sudo -i (login shell flag, takes no arg) vs env -i (cleared env, takes no arg) both are handled. One edge: sudo -u root -- sed ... — the -- inside the wrapper loop breaks option consumption, correct. And time -p etc. are fine. No issue found; the nohup sudo -u root sed -i ... test covers nested wrappers.
  5. _shell_word_split deliberately drops quotes; a sed program containing the protected path quoted (e.g. sed -i 's|~/.hermes/config.yaml|x|' README.md) is correctly not treated as an operand — covered by tests. Good.
  6. Minor: _detect_hermes_inplace_edit iterates command_variant.split("\n") — multi-line commands with a newline between cd and sed also escape the cd-hint idea from Support passing morph snapshot id #2; consistent with the "out of scope" posture, just worth being explicit.

@alt-glitch alt-glitch added the tool/terminal Terminal execution and process management label Aug 15, 2026
@egilewski

Copy link
Copy Markdown
Contributor

suggesting changes

Two P2 hardline bypasses remain in the protected-file edit guard:

  • [P2] Wrapper options can hide the real editor
    The wrapper resolver does not consume every option that takes an operand before the editor, and it omits command-position wrappers such as timeout. A wrapped in-place edit of a protected policy or credential file can avoid the hardline detector and proceed through a bypass mode. Extend the wrapper grammar to cover all supported short and long options, including attached and equals forms, recognize command-position wrappers, and add regression coverage for each wrapper before the editor.

  • [P2] Path aliases bypass protected-file matching
    Protected operands are matched textually without normalizing POSIX dot segments and repeated separators. Aliases of the same policy or credential file can therefore evade the hardline floor. Canonicalize each resolved operand before exact filename-boundary matching while retaining the active home mapping, and add positive alias cases plus backup-name negatives.

Security evidence:

  • trust boundary: The terminal approval guard mediates untrusted shell execution, and protected policy and credential files must remain below all bypass modes.
  • source/sink/invariant: The detector resolves wrappers, parses in-place editor options, and compares protected operands; every in-place edit of those files must be rejected before any bypass mode.
  • current-main reproduction: The baseline leaves wrapper-option, command-position-wrapper, and path-alias variants unblocked; the reviewed change blocks direct canonical edits but does not close these variants.
  • PR-head or patch-replay validation: The reviewed implementation and focused regression coverage confirm the direct protection while leaving the reported wrapper and path-alias variants reproducible.
  • positive/negative cases: Canonical protected edits are blocked, while reads, unrelated files, backup names, and protected text used as data remain allowed.
  • residual bypass search: Unrecognized command-position wrappers, wrapper options that take operands, and POSIX path aliases remain actionable.
  • reviewer validation: Focused regression checks and source review support all reported findings.

Not checked:

  • CodeRabbit review
  • Indirect target delivery
  • Variable-delivered paths

Signed: GPT-5.6-luna-max in Codex

@alt-glitch alt-glitch added P3 Low — cosmetic, nice to have and removed P2 Medium — degraded but workaround exists sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Aug 15, 2026
…onto origin/main)

Collapse R1-R3 into one commit on current origin/main (36b0a96), replacing
the ~2700-commits-stale base. Clean 3-way merge — no textual conflicts, no name
collisions with upstream's a7aa814/8163c8731b; every piece of the R1-R3 work
verified present in the merged tree.

The fix (unchanged in content):
- _wrapper_operand_span() + _leading_wrapper_indexes(): one shared wrapper
  model consumed by BOTH the floor resolver and the -c-payload stepper, so the
  two detection paths cannot drift (the R3 bypass class).
- _flock_exec_payload(): flock's own -c (runs payload via sh -c).
- _WrapperResolutionFailed + fail-closed path in _detect_hermes_inplace_edit().
- posixpath.normpath in _is_protected_hermes_operand() (path-alias canonicalization).
- Complete _EDITOR_WRAPPERS / _WRAPPER_POSITIONAL / _WRAPPER_OPTS_WITH_ARG /
  _WRAPPER_NOARG_FLAGS tables (man-page verified).

Verification (on the NEW base):
  targeted file:      354 passed, 0 failed
  reviewer probe:     PASS (61/61 in-place, 13/13 unrelated, 23/23 allow)
  approval-domain:    test_approval 2/106, test_execution_flag_detection 4/78/3sk,
                      test_credential_files 3/39/1sk, test_approved_command_clean_slate
                      3/5 — all byte-identical to origin/main baseline
@qni-live
qni-live force-pushed the fix/hardline-hermes-config-inplace-edit branch from 66b596e to 1f004a9 Compare August 27, 2026 11:10
@qni-live

Copy link
Copy Markdown
Author

Thanks — both P2 findings reproduced as described and are now closed. Force-pushed
a single commit rebased onto current main (1a66134404b8).

[P2] Wrapper options can hide the real editor

Reproduced. The wrapper resolver skipped only option-shaped tokens, so a wrapper
whose own operand is positional was not modelled at all, and the option tables
were incomplete for the wrappers that were.

Rather than extend the tables again, the resolver now carries an explicit wrapper
grammar: for each wrapper, the options that consume a following token, and the
number of mandatory positionals before the command word. timeout, flock,
taskset and chrt are modelled as command-position wrappers on that basis.
chrt's optional priority is disambiguated by shape (numeric = priority,
non-numeric = command word), matching chrt(1).

Auditing the existing table against the man pages turned up four entries that do
not exist and were removed: ionice --nice and --priority (the long form of
-n is --classdata), sudo --role and --type (not in sudo 1.9.18), and
doas --user. Missing argument-taking options were added in the same pass —
timeout -s, ionice -u/--uid, sudo -D/-R/-T/-U, doas -a, time -f/-o,
and the flock/taskset/chrt sets, which had no entries at all.

[P2] Path aliases bypass protected-file matching

Reproduced. Operands are now canonicalized before the filename-boundary
comparison — repeated separators collapsed, . and .. segments resolved,
trailing separator stripped — using pure posixpath string normalization. No
realpath, no filesystem access: this runs on adversarial input inside a guard.
The home mapping is applied first, so ~/, $HOME/, ${HOME}/, $HERMES_HOME/
and the spelled-out /home/<u>/ and /Users/<u>/ forms all normalize alike.

Backup names stay out: config.yaml.bak and config.yaml.orig are distinct
files carrying no policy. ~/.hermes/../other/config.yaml normalizes out of the
protected directory and is allowed.

Two structural changes, both prompted by this review

Fail closed on an unresolvable wrapper. Keeping two hand-maintained option
tables in sync is not a property the code enforces, and a missing entry fails
open — the argument is miscounted as a positional and the resolver returns the
wrong command word, silently. _resolve_command_word() now raises on any
unrecognized option shape. _detect_hermes_inplace_edit() blocks such a segment
only when it still names an in-place editor and a protected file, so
flock --bogus 5 /tmp/lock cat … remains allowed.

One wrapper model, two call sites. The floor resolver and the payload stepper
behind _execution_flag_findings() maintained separate wrapper sets — 16 names
against 8. Any wrapper present in one but not the other blocked the direct form
while letting the shell-payload form through: timeout 5 sed -i … config.yaml
was denied, timeout 5 bash -c 'sed -i … config.yaml' was not, and the same held
for flock, taskset and chrt, including flock FILE -c …, which flock(1)
runs through a shell. Both paths now consume the same tables, so that divergence
cannot recur. _COMMAND_WRAPPER_WORDS, _SUDO_OPTIONS_WITH_ARG and
_ENV_ASSIGNMENT_RE are removed as part of the merge.

Validation

  • tests/tools/test_hardline_blocklist.py: 354 passed, 0 failed.
  • Every reported and derived case replayed through check_all_command_guards()
    under HERMES_YOLO_MODE=1: 61 in-place block cases denied with
    hardline=True; yolo does not lift the floor.
  • Blast radius of the shared wrapper model: 13 unrelated hardline rules
    (rm -rf /, mkfs, reboot, init 0, fork bomb, dd to raw device) still
    fire, bare and behind wrappers, including via bash -c payloads.
  • Allow side: 23 cases clean, including quoted prose, reads, backup names,
    program-text mentions of the protected path, and unresolvable-wrapper
    negatives.
  • Regression baseline: test_approval.py, test_execution_flag_detection.py,
    test_credential_files.py and test_approved_command_clean_slate.py give
    228 passed / 12 failed / 4 skipped both with the change and on the unmodified
    rebase target. The 12 are pre-existing Windows-environment failures on this
    machine. The full tests/tools/ run was not carried to a clean summary
    locally, so it is not claimed as green.

Known limits, unchanged in scope

Indirect target delivery still stays at the smart level, consistent with the
xargs rm / find -exec rm rules: xargs, find -exec, and variable-delivered
paths. The cd-relative operand (cd ~/.hermes && sed -i 's/a/b/' config.yaml)
is one precise instance of that class — closing it needs working-directory state
tracked across the && boundary — and is now named explicitly in the code
comment rather than left implicit. flock's file-descriptor form (flock 9 cmd)
has no FILE positional and is not modelled; noted in the comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/auth Authentication, OAuth, credential pools comp/tools Tool registry, model_tools, toolsets P3 Low — cosmetic, nice to have sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit tool/terminal Terminal execution and process management type/security Security vulnerability or hardening

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants