Run skylos . --diff-base origin/main --danger --quality on the branch: a deleted @login_required, CSRF, rate-limit or validation check comes back as SKY-L021. Not every Skylos mode reports it, though; the table below shows which ones do in 4.47.1.

If you review enough AI-generated pull requests, you see the same pattern. The code is clean, the refactor is plausible, the diff is large, and the vulnerability is a thing that is no longer there:

  • a deleted @login_required
  • a missing permission branch
  • input validation that no longer runs
  • rate limiting that vanished during route cleanup
  • a middleware registration that quietly dropped out

That is why AI PR review needs a regression mindset, not just an issue-spotting one.


Which commands report a removed control (4.47.1)

The test branch removes @login_required (and its import) from a Django view and changes nothing else:

-from django.contrib.auth.decorators import login_required
 from django.http import JsonResponse
 
 from app.export import export_rows
 
 
-@login_required
 def export_customer(request, customer_id):
     return JsonResponse({"rows": export_rows(customer_id)})
Command, run on the branchSKY-L021 reported?Exit code
skylos . --diff-base main --danger --qualityYes0
skylos . -a --diff-base mainYes0
skylos . --diff-base main --danger --quality --gateYes0 (the default gate doesn't fail on it)
skylos agent pre-commit (change staged, not committed)Yes1 (commit refused)
skylos . --danger --quality (change still uncommitted)Yes0
skylos . --danger --quality (change committed)No0
The same scan with --diff main instead of --diff-base mainNo0
skylos . --diff-base main --diff main ... (what skylos cicd init generates)No0, and skylos cicd gate passed
skylos done --base mainNo0, Verdict: PASS

Two reasons. --diff keeps only findings on changed lines, and a deleted decorator leaves no changed line behind. And a plain scan only diffs uncommitted and staged changes against HEAD; once the change is committed, it has nothing to compare. The 4.47.2 development build gave the same results.

This is what the --diff-base scan printed in --format concise (dead-code lines omitted):

app/views.py:5  SKY-L021  Security control regression: Auth decorator @login_required was removed

What SKY-L021 recognizes

The rule compares the removed and added lines of a diff. It skips test files, and every finding is reported as SKY-L021 (HIGH) with the message prefix "Security control regression:".

ControlChange that triggers itWhat Skylos reports
Auth decoratorsremoving @login_required, @permission_required, @permissions_required, @require_auth, @requires_auth, @authenticated, @jwt_required or @token_requiredAuth decorator @login_required was removed
FastAPI auth dependenciesremoving Depends(get_current_user), Depends(get_current_active_user), Depends(require_admin) or Depends(verify_token) without adding it backAuth dependency Depends(get_current_user) was removed
Express auth middlewareremoving middleware such as requireAuth, isAuthenticated, verifyToken or passport.authenticate from a route that still existsAuth middleware 'requireAuth' was removed from app.get('/export')
CSRFremoving @csrf_protect, CSRFProtect or CsrfViewMiddleware, or adding @csrf_exemptCSRF protection 'csrf_protect' was removed
Rate limitingremoving @rate_limit, @ratelimit, @throttle or @limiter.limitRate limiting decorator @rate_limit was removed
TLS verificationadding verify=False, especially where verify=True was removedTLS verification downgraded from verify=True to verify=False
Hash strengthreplacing sha256, sha512, bcrypt, argon2, scrypt or pbkdf2 with md5 or sha1Crypto downgraded from sha256 to md5
Validationremoving @validate, @validator, @field_validator, @validates or @validates_schema, a validate( or sanitize( call, or a Django form or DRF serializer validatorValidation decorator @field_validator was removed
Security headersremoving Content-Security-Policy, Strict-Transport-Security, X-Frame-Options or another named security header, or SecurityMiddleware, helmet( or secure_headersSecurity header 'Content-Security-Policy' was removed
Encryptionremoving Fernet, AES, encrypt( or decrypt( calls, or a SECRET_KEY = assignmentEncryption call was removed
Audit loggingremoving @audit, @log_access or an audit_log( callAudit/logging call was removed
Sanitizationremoving html.escape(, bleach.clean(, markupsafe.escape( or DOMPurify.sanitize( without adding it backSanitization call was removed
Permission checksremoving has_permission(, check_permission(, has_perm(, user_passes_test, @permission_classes or @has_rolePermission check was removed

Names outside these lists are not recognized. A custom decorator such as @staff_only needs a test or a manual review step.


Run it on every pull request

This job fails the pull request when the scan reports a removed control. Run locally against a clone with GITHUB_BASE_REF=main, the scan-and-grep step exited 1 on the removal branch and 0 on main:

name: skylos-removed-controls
on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  removed-controls:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false
      - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
        with:
          python-version: "3.12"
      - run: python -I -m pip install "skylos==5.1.1"
      - name: Fail on removed security controls (SKY-L021)
        run: |
          skylos . --diff-base "origin/$GITHUB_BASE_REF" --danger --quality --format json -o skylos.json
          if grep -q '"SKY-L021"' skylos.json; then exit 1; fi

--gate on its own would not fail this job: SKY-L021 is counted as a quality finding, and the default gate allows up to 10 of those. The last line fails the job whenever the scan reports a removed control. fetch-depth: 0 makes the base branch available for --diff-base. Make the job a required status check.

If you also use skylos cicd init --no-upload, keep this job alongside the generated "Skylos Quality Gate" and "Skylos Done" jobs. The generated gate scans changed lines (--diff), so on its own it does not fail a PR for a removed decorator in 4.47.1.


Block it before the commit

For changes made on a developer's machine, including by an agent, skylos agent pre-commit checks the staged files and refuses the commit:

# .git/hooks/pre-commit (chmod +x)
#!/bin/sh
exec skylos agent pre-commit
Commit check: reviewing 1 source staged file(s). Checks security, secrets, and
high-signal quality regressions on production source/config.
1 issue(s) found in staged files (1 quality):
  HIGH app/views.py:5 Security control regression: Auth decorator
@login_required was removed

git commit --no-verify skips any pre-commit hook, so treat this as fast feedback and keep the CI job. The agent hooks guide has a PreToolUse hook that refuses --no-verify in Claude Code.


Review regressions before additions

In AI-generated PRs, deleted protections deserve priority over newly added low-severity findings:

  1. removed auth or permission logic
  2. removed validation or output safety
  3. removed rate limiting, CSRF, or middleware
  4. newly introduced dangerous sinks
  5. dead code and cleanup noise

That ordering keeps a serious removal from getting lost in 200 lines of helper extraction.

Review questions that match the failure mode:

  • What existing protection disappeared in this refactor?
  • Did any route or handler lose auth, permission, or rate-limit enforcement?
  • Did validation move in a way that now skips some code paths?
  • Did middleware or settings registration change?
  • Is any deleted code required for security or auditability?

Removed tests are a regression too

The same refactor can delete or weaken the tests that would have caught it. skylos done --base origin/main flags deleted, skipped and assertion-free tests, loosened pytest settings and CI test steps, and test-only special cases (how it works). It does not report removed auth checks, so run it alongside the scan above, not instead of it.


When SKY-L021 fires

Restore the control. If the removal is intended (the route moved behind middleware, say), record why in the PR and suppress that line with # skylos: ignore[SKY-L021]. Reviewers then see a decision instead of a silent deletion.

Treat the PR as high-risk if it also:

  • rewrites auth, billing, admin, or export flows
  • touches decorators, middleware, or route registration
  • replaces several files with one "simplified" abstraction
  • changes request parsing or serializer behavior
  • removes tests while refactoring the protected path

What it does not catch

SKY-L021 only knows the named patterns in the table above. It does not see a tenant filter dropped from a query, business authorization rules, CORS policy, or arbitrary middleware behavior. Cover those with tests that exercise the protected path as an unauthorized user, and with manual review of any diff that touches auth, permissions or request handling.


Where to go next

Running this across a team? These checks are free in the CLI. Skylos Cloud keeps the results in one place: the Free plan adds a pass/fail check on pull requests and scan history for one project, and the Workspace plan adds up to 10 projects, approvals for findings you decide not to fix, pull request comments, and audit exports.