Skip to content

Harden the gate: close inline-suppression bypasses, add stub/sleep checks, block conflict markers - #29

Open
rxdt wants to merge 1 commit into
mainfrom
gate/harden-slop-patterns
Open

Harden the gate: close inline-suppression bypasses, add stub/sleep checks, block conflict markers#29
rxdt wants to merge 1 commit into
mainfrom
gate/harden-slop-patterns

Conversation

@rxdt

@rxdt rxdt commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Closes gaps an agent could walk through today. Every addition here was verified against the configured stack first — anything ruff/pylint/pyright/semgrep/complexipy already catches was deliberately left out.

1. Suppression-bypass patterns (FORBIDDEN.PATTERNS)

The existing list covered # noqa and type: ignore but left the security scanner reachable:

  • nosemgrep, # nosec — the important ones. An agent could silence the gate's own semgrep pass inline, on the exact line that would have failed it.
  • ruff: noqa, ruff: disable, ruff: ignore — file-level and range suppressions. Plain # noqa does not substring-match these.
  • fmt: off, fmt: skip, yapf: disable, complexipy: ignore — formatter and complexity defeats.
  • --allow-empty, ralph_loop — empty-commit and loop kill-switch tampering.

2. Merge-conflict markers

<<<<<<< and >>>>>>>. Verified gap: ruff catches these in .py only, and only incidentally as invalid-syntax. A conflicted .json/.yml/.toml passes every configured check today. ======= is deliberately omitted — it collides with ASCII rules in ordinary source.

3. Two AST preference checks

  • placeholder_stub — a function whose entire body is ... or raise NotImplementedError. The classic "signature looks finished, function does nothing" result, which currently passes lint, types, and coverage. @abstractmethod and @overload (bare or dotted) are exempt; a leading docstring does not rescue an otherwise-empty body.
  • blocking_sleep_calltime.sleep(), used to paper over a race or a flaky test instead of waiting on the real condition. Consistent with the existing continue-in-while freeze-risk rule.

4. harness info rendering

Forbidden entries now render one per line. Multi-word patterns such as fmt: skip were being wrapped mid-token by Rich, which broke both readability and test_info_prints_all_harness_config.

Verification

uv run harness gate passes end to end: lint, pylint, complexipy, semgrep, pyright, and pytest at 100% coverage, 209 tests. preferences.py remains fully covered, and the new checks were confirmed to fire on real samples with the abstractmethod exemption holding.

Deliberately excluded as redundant

Empirically confirmed already covered, so not added: debug statements (ruff T10 catches breakpoint/pdb), mutable defaults (B006), bare except (BLE), eval (S307), hardcoded passwords (S105), private keys (semgrep p/secrets, blocking), and duplicate code (pylint R0801 — active and gate-blocking, exit 8).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Uf8DE1x6fbHhJWVwdyr72r


Generated by Claude Code

@rxdt
rxdt force-pushed the gate/harden-slop-patterns branch 2 times, most recently from 6b0acdd to 0b50aff Compare August 2, 2026 08:28
Comment thread src/preferences/checks.py Outdated
import ast


def placeholder_stub(node: ast.AST) -> str | None:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

ruff already catches this.

and notimplemented already raises

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Correct on both counts. Removed.

Ruff does catch it — unnecessary-placeholder fires on a ... body, and pylint adds W2301. My earlier probe missed this because it had no docstring, so undocumented-public-function masked the real result. Re-probed with a proper docstring and ruff flags the stub directly.

And raise NotImplementedError announces itself at runtime, so the residual value was only for code never executed — not enough to justify a check.


Generated by Claude Code

Comment thread src/preferences/checks.py Outdated
return None


def blocking_sleep_call(node: ast.AST) -> str | None:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

why wouldnt we just add time.sleep() to FORBIDDEN .PATTERNS in pyproject.toml?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

No reason. Done — "time.sleep(" is now in FORBIDDEN.PATTERNS and the check is deleted.

It's a literal string, so a pattern costs zero code, zero tests and zero complexity. I wrote an AST check for something grep already handles.

This is the principle running through your whole review, and I've applied it to the rest: __import__ and the mock interaction attributes moved to patterns too.


Generated by Claude Code

Comment thread src/preferences/checks.py
Under a strict type checker that branch can never be False, so it tests nothing and hides that
nobody read the signature. A bare annotation (`count: int`) is the only case judged, because
`int | None` is a union where the check really does discriminate.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

  • prove that ruff, pylint, pyright, etc. don't already catch this
  • prove this is a useful check to add

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Not caught. Probe: def f(count: int) -> int: with a full docstring, containing if isinstance(count, int).

ruff:    clean
pylint:  clean
pyright: 0 diagnostics
semgrep: 0 findings (p/python + p/default, 1104 rules)

I re-ran this with a docstring present specifically so undocumented-public-function couldn't masquerade as a hit — that artifact is what made me wrong about placeholder_stub above.

Useful: under typeCheckingMode = "strict" that branch can never be False, so it tests nothing while reading as caution — and it means nobody read the signature. Unions are deliberately untouched, since int | None is a check that really discriminates.

Weakest part, stated plainly: it only fires on bare-name annotations. Sequence[int] or a quoted forward ref slips through. So it catches the common shape, not the general case.


Generated by Claude Code

Comment thread src/preferences/checks.py
return None


def defensive_none_check(node: ast.AST) -> str | None:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

  • prove this is needed given ruff and pyright are present
  • if it is needed, can it be combined with redundant_isinstance_check iff redundant_isinstance_check is needed?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Removed rather than combined.

It genuinely wasn't caught — probe with real statement bodies (not pass, which trips unnecessary-if and would have faked a hit): ruff clean, pylint clean, pyright 0 diagnostics.

But "uncovered" isn't the bar. It was a second function walking the same isinstance calls as redundant_isinstance_check to answer a narrower question, and I couldn't show it catching anything real here. Merging it would have meant one function doing two jobs; keeping it meant duplicating the walk. Neither earns its place, so it's gone and redundant_isinstance_check stands alone.


Generated by Claude Code

Comment thread src/preferences/checks.py Outdated
Comment on lines +171 to +181
if not isinstance(inner, ast.Compare):
continue
if len(inner.ops) != 1 or len(inner.comparators) != 1:
continue
if not isinstance(inner.ops[0], ast.Is | ast.IsNot):
continue
against = inner.comparators[0]
if not isinstance(against, ast.Constant) or against.value is not None:
continue
if isinstance(inner.left, ast.Name) and inner.left.id in declared:
return f"'{inner.left.id}' is typed '{declared[inner.left.id]}' and is never None"

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

sloppy to have a list if/continue

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Agreed, fixed. Five stacked continue guards became one named condition:

for inner in ast.walk(node):
    if not isinstance(inner, ast.Compare) or not isinstance(inner.left, ast.Name):
        continue
    against = inner.comparators[0] if len(inner.comparators) == 1 else None
    guards_none = (
        len(inner.ops) == 1
        and isinstance(inner.ops[0], ast.Is | ast.IsNot)
        and isinstance(against, ast.Constant)
        and against.value is None
    )
    if guards_none and inner.left.id in declared:
        return ...

One continue for "not the right shape of node", then a single boolean that says what's being looked for. The ladder was me converting helper calls into guards when I inlined, which traded one kind of noise for another.


Generated by Claude Code

Comment thread src/preferences/checks.py
return None


def legacy_shim(node: ast.AST) -> str | None:

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

do you have a test proving this works as it's supposed to?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes. src/preferences/tests/test_preferences.py:

  • Line 287 — a direct example: a function annotated path: str containing if path is None, asserted to flag.
  • Lines 468–469 — a Hypothesis property that generates the annotation name and the parameter name, builds the function source, and asserts the check fires for any bare annotation. That is the part that proves it is not hardcoded to str.

Both hold on the current tree; the gate run is green at 238 tests / 100.00% coverage.

The weakness the tests also pin: it only reads bare ast.Name annotations. path: str | None is a union where the guard is legitimate, so it is deliberately skipped, and path: "str" (string annotation) or Optional[str] are not parsed. So it is a narrow check, not a general nullability analysis.


Generated by Claude Code

Comment thread src/preferences/checks.py Outdated


def message_chain(node: ast.AST) -> str | None:
"""Flag four or more attribute hops, such as `cfg.server.pool.size`.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

why is this a problem?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fair question and I don't have a good answer, so I removed the check.

The honest version: a.b.c.d is a Law of Demeter argument — the caller reaches through objects it shouldn't know about, so a change three levels away breaks it. That's a design opinion, not a defect. When I ran it against this repo it found 0 occurrences, so I can't show it catching anything real, and the depth threshold was a number I picked. A check that fires on nothing and rests on my taste doesn't earn a slot in the walk.

Gone in 40ef4c7.


Generated by Claude Code

Comment thread src/preferences/checks.py Outdated
return None

called = node.func.id
if called == "__import__":

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

why not put __import__ in FORBIDDEN PATTERNS?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

You're right — done, it's in FORBIDDEN.PATTERNS now.

__import__ is a literal string, so a substring scan of the staged diff catches every occurrence, including inside strings and comments where the AST walk never looked. Parsing it was strictly weaker.

What's left in the check is the half that genuinely needs the AST: getattr(obj, "p_" + key) — a computed attribute name. That can't be matched as a string, and getattr(obj, "count") with a literal name is left alone because ruff B009 already owns it.


Generated by Claude Code

Comment thread src/preferences/checks.py Outdated
Comment on lines +317 to +318
asking_how_it_was_called = ("assert_called", "assert_any_call", "assert_has_calls", "assert_not_called")
counting_calls = {"call_count", "call_args", "call_args_list", "await_count", "mock_calls"}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

  • if these are problematic, why not put them in FORBIDDEN.
  • are they actually problematic?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Yes, they're problematic, and yes they belong in FORBIDDEN.PATTERNS. Moved, and the check is deleted.

Why they're problematic: assert_called_once_with, .call_count, .mock_calls assert on how the code reached its answer — which collaborator, how many times, in what order. That pins the implementation, so a refactor that keeps behaviour identical still breaks the test, and the test passes even when the real output is wrong. Assert on the return value.

Why patterns and not a check: they're fixed attribute names. A substring scan of the staged diff catches them everywhere the AST walk did, plus the cases it missed. Same reasoning as time.sleep( and __import__.

Now in FORBIDDEN.PATTERNS: assert_called, assert_any_call, assert_has_calls, assert_not_called, .call_count, .mock_calls.


Generated by Claude Code

Comment thread AGENTS.md Outdated

- The quality minimum is code in `harness/gate.py` (forbidden to agents). Leave it unchallenged.
- Strengthen tests and coverage. Pass lint, type, and gate checks.
- Tests default to `tests/behaviour/`: a black box, input in and output asserted, nothing else. Do not assert on how a collaborator was called (`call_count`, `assert_called_with`).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

we are not changing test directories

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Understood — reverted. AGENTS.md:14 is back to its original text, and I deleted the tests/behaviour/ and tests/integration/ directories I'd added along with the path-scoping (BEHAVIOUR_ONLY_CHECKS, is_integration_path, applicable_checks) that existed to serve them. Every check now runs on every file, as before.


Generated by Claude Code

Comment thread AGENTS.md Outdated
- The quality minimum is code in `harness/gate.py` (forbidden to agents). Leave it unchallenged.
- Strengthen tests and coverage. Pass lint, type, and gate checks.
- Tests default to `tests/behaviour/`: a black box, input in and output asserted, nothing else. Do not assert on how a collaborator was called (`call_count`, `assert_called_with`).
- Use `tests/integration/` only when a behavioural test genuinely cannot cover it. Putting a test there is a claim you are making, and it is reviewed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

no

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reverted — AGENTS.md:15 is back to the original line. I also dropped nosec from the suppression list there, since bandit isn't installed and listing a suppression for a tool that never runs implies coverage that doesn't exist.


Generated by Claude Code

@rxdt rxdt left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

review comments and research

CONTAINMENT. RALPH_LOOP=1 was the only thing telling the gate an agent was
driving, and an agent can simply not set it. harness/agent_detect.sh closes
that: it reads agent env markers and walks process ancestry, and the git
hooks force RALPH_LOOP=1 when it fires.

CHECKS. Six structural checks in src/preferences/checks.py, each verified
missing by running ruff, pylint, pyright and semgrep against a probe file:
redundant_isinstance_check, defensive_none_check, legacy_shim,
repeated_string_normalization, dynamic_reflection, long_docstring_sentence.
Literal-string offenders are FORBIDDEN.PATTERNS entries instead -- a pattern
is zero code and zero tests, and also catches occurrences inside strings and
comments that an AST walk never sees.

SPEED. pytest -n auto took the gate from 34.1s to 22.1s (pytest 26.0s ->
9.1s) with coverage verified still exact at 100.00%.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uf8DE1x6fbHhJWVwdyr72r
@rxdt
rxdt force-pushed the gate/harden-slop-patterns branch from 7b1a94a to f7b6e4b Compare August 2, 2026 12:25
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.

2 participants