Skip to content

fix(analyzer): treat a command wrapper with no command as complete - #779

Open
elliottwaves-20 wants to merge 1 commit into
NVIDIA:mainfrom
elliottwaves-20:fix/694-wrapper-without-command
Open

elliottwaves-20 wants to merge 1 commit into
NVIDIA:mainfrom
elliottwaves-20:fix/694-wrapper-without-command

Conversation

@elliottwaves-20

@elliottwaves-20 elliottwaves-20 commented Oct 6, 2026 •

Copy link
Copy Markdown

Summary

A Markdown parameter table such as

| Name | Type | Default | Description |
|---|---|---|---|
| timeout | float | 150 | Seconds to wait |

makes has_bounded_parse_exhaustion (static_patterns_tool_misuse) report static_parse_limit, so the file is recorded as partially inspected and every SKILL.md reference to it becomes an AE1. The same happens in host code such as signal.alarm(timeout) or if (timeout) {. Refs #694 (the Markdown table-cell case reported there).

Root cause

_shell_clause_starts opens a new clause after every | and (, so timeout above is read as the wrapper command timeout. Its next token is the control operator |, so _next_shell_invocation_word returns None. In _command_string_from_clause the sudo/nice/xargs and timeout branches answer None with (True, None), which means "unresolved command string" and leads to exhaustion. The env/command/nohup path answers the same situation with (False, None) (no command). That is why timeout, sudo, nice and xargs reproduce and env, nohup and command do not, as noted in #694. timeout 5 | was already complete, because there the empty command is seen by the generic path.

Fix

The wrappers that report no command string are sudo, nice, xargs and timeout. They do so when their next token is ;, |, ) or &, as long as the & is not followed by >. None of these wrappers can run another command at that point: sudo and timeout fail with usage, nice prints the niceness, and xargs defaults to echo. A new helper _ends_wrapped_clause decides this.

Unchanged (stay partial):

  • A redirection before the wrapped command (sudo >log $CMD -rf /, sudo <in …, sudo &>log …).
  • (, and the end of the view, where a fragment may continue.
  • Any wrapper option or -- handling.
  • sh -c/eval command strings.
  • Every rule finding from analyze(). For example, `sudo rm -rf /` in a table cell still produces TM1.

Tests

tests/nodes/analyzers/test_command_wrapper_without_command.py has 29 tests. 17 of them fail on main, and all 12 fail-closed controls pass on both.

  • Benign ⇒ complete:
    • Markdown cells with timeout, sudo, nice and xargs, with and without a long tail.
    • An API parameter table.
    • Shell sudo | cat, sudo |& cat, timeout; echo, timeout && echo, xargs || true, nice & wait, (nice), a nice ;; case item and sudo -u | cat.
    • Python signal.alarm(timeout) and JavaScript if (timeout).
  • Controls ⇒ partial: redirections (>, <, &>), timeout ($CMD) -rf /, x | sudo sh -c "$CMD", sudo |& sh -c "$CMD", sudo && eval "$CMD", sudo -s; eval "$CMD", case $a in sudo) $CMD -rf / ;; esac, and a wrapper at the end of the view (complete and fragment context).
  • Findings kept: a table cell with `sudo rm -rf /` keeps TM1, and `sudo $CMD -rf /` stays partial.
  • Scan level: run_static_patterns_with_ledger gives COMPLETED for the table and PARTIAL/STATIC_PARSE_LIMIT for the redirected wrapper.
  • Full unit suite: the set of failures is identical with and without this change, which I ran together with fix(analyzer): end an expansion word at its enclosing double quote #780 (double-quoted expansion case) (10108 passed). The remaining failures and errors on this Windows machine also occur on unmodified main and are unrelated: release/CLI path tests and three parametrized collection errors.
  • ruff check and ruff format --check are clean.
  • Real files: I ran a static check over 4000 files from about 200 public skills, including their vendored venvs. 29 files go from partial to complete, for example urllib3/connectionpool.py, grpc/_channel.py and two Tavily reference pages. No file goes from complete to partial, and the TM1 count is unchanged (60).
  • Differential fuzzing against main: I generated about 3000 random shell fragments built from wrappers, operators, quotes, substitutions and rm/-rf / pieces. Every input that went from partial to complete was checked. The ones that bash -n accepts are heredoc text, comments or wrappers without a command, such as timeout |echo and $(sudo ). No input went from complete to partial.
  • The change merges cleanly with fix(static): stop code comparisons from reading as tag marker directives #777 and fix(analyzer): keep Python and Perl syntax from exhausting the shell parser #778. Applied on top of fix(analyzer): keep Python and Perl syntax from exhausting the shell parser #778, its 52 tests and this PR's tests pass together.

Residual

sudo -s/sudo -i before a control operator now counts as "no command string", the same as sh at the end of a pipeline already does. Stdin-fed shells are outside what this check models, and the docstring says so.

#778 also handles the timeout wrapper names inside valid Python through Python token ownership. This PR covers the same root cause for every file type, including Markdown, JavaScript and Python fragments, and does not conflict with #778.

🤖 Generated with Claude Code

A clause such as `| timeout |` in a Markdown parameter table, or
`signal.alarm(timeout)` in host code, makes `_command_string_from_clause`
see the wrapper `timeout` (also `sudo`, `nice`, `xargs`) followed directly
by a control operator. Those branches returned "unresolved command
string", so the file was recorded as `static_parse_limit`. The `env`,
`command` and `nohup` branches already treat the same situation as "no
command".

When the wrapper's next token is `;`, `|`, `)` or `&` (but not `&>`), no
other command can run, so report no command string. A redirection, a `(`,
`&>` and the end of the view stay unresolved.

Refs NVIDIA#694

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: elliottwaves-20 <pail1217@web.de>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant