Skip to content

Bug: @query timeout=N modifier can leak into executed shell commandΒ #138

@tcconnally

Description

@tcconnally

Severity: πŸ”΄ Critical (silent breakage of @query)

In src/perseus/directives/query.py, the timeout=N modifier is stripped from raw AFTER cmd was extracted (lines ~75–85):

cmd_match = re.match(r'^"((?:[^"\\]|\\.)*)"', raw)   # ~75
...
else:
    cmd = cmd_raw                                    # ~85
...
timeout = int(cfg["render"].get("query_timeout_s", 30))
tm_match = re.search(r'\s+timeout=(\d+)(?:\s|$)', raw)   # ~98
if tm_match:
    timeout = int(tm_match.group(1))
    raw = (raw[:tm_match.start()] + raw[tm_match.end():]).rstrip()  # mutates raw, not cmd

For unquoted commands, cmd = cmd_raw captures the entire raw including timeout=10. The re.sub only modifies raw, never cmd. The executed shell command therefore contains the literal text timeout=10.

Repro

@query git status timeout=10

Expected: git status runs with 10s timeout.
Actual: git status timeout=10 runs (invalid; subprocess returns nonzero with confusing error).

Suggested fix

Hoist modifier extraction above cmd extraction:

# Extract timeout=N modifier first
timeout = int(cfg["render"].get("query_timeout_s", 30))
tm_match = re.search(r'\s+timeout=(\d+)(?:\s|$)', raw)
if tm_match:
    timeout = int(tm_match.group(1))
    raw = (raw[:tm_match.start()] + raw[tm_match.end():]).rstrip()

# Now extract command
cmd_match = re.match(r'^"((?:[^"\\]|\\.)*)"', raw)
...

Apply the same hoisting discipline for fallback= and schema= if they could similarly leak.

Acceptance criteria

  • Test: @query git status timeout=5 β†’ cmd == "git status", timeout == 5.
  • Test: @query "echo foo timeout=10" β†’ cmd == "echo foo timeout=10" (literal inside quotes), default timeout.

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions