Skip to content
Web apps

Django 6 guard for removed APIs and django-upgrade

Stop agents writing url(), ugettext, is_ajax(), index_together and CheckConstraint(check=) into Django 6.1 projects, then run django-upgrade and Django's own checks.

Verified install Not agent-tested v1.0.0 MIT updated djangopythonuv
Equips
  • 2 2 Rules
  • 1 1 Skills
  • – 0 MCP

Agents learnt Django from a decade of answers, so they still write url(), ugettext_lazy, force_text, request.is_ajax(), Meta.index_together and {% load staticfiles %}. All of those are gone, and Django 6.0 and 6.1 removed more: CheckConstraint(check=), positional Model.save() arguments, format_html() with no arguments, finders.find(all=) and the Postgres aggregates' ordering=. Django 6.1 also moves mail settings into MAILERS and adds fetch modes.

This loadout pairs a short Cursor rule and AGENTS.md (a never/always table and the Django 6 version facts) with a skill that checks the work in three layers. A standard-library Python scanner reports removed APIs as errors and behaviour changes as warnings. django-upgrade rewrites what it safely can. Then manage.py check, makemigrations --check and the tests run with the next two deprecation warnings raised as errors. The verifier runs all of it on a small Django 6.1 shop project and a Django 3-era sample, with the pinned Django 6.1.2 and django-upgrade 1.33.0.

What's in it

  • Never/always table for Django 6.1 in a Cursor rule for .py and templates
  • Scanner flags 17 removed Django APIs as errors, read-only
  • Runs django-upgrade --check at target 6.1 alongside the scanner
  • Tests run with RemovedInDjango70Warning and RemovedInDjango2028Warning as errors
  • Covers 6.1's MAILERS setting and FETCH_RAISE for N+1 queries

You'll need

  • Tools: python3, uv
Level 1 · automated

Verification

Verified install

Checked at commit c1f7b63 · 9 passed, 0 failed, 1 not applicable

CheckResultWhat we found
Listinglisting.yaml matches the schema and declares every file in the folder. Pass
Schema valid; 8 files declared, none missing or extra
  • tested.status: untested (agent tests are separate from Level 1)
Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. Pass All 8 files parse; front matter valid
AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. Pass
CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
  • CLAUDE.md: matches the generator
  • GEMINI.md: matches the generator
Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. Pass
Correct paths for all 6 agents; layouts re-discovered
  • Claude Code: 6 files, 1 skill in .claude/skills/
  • Codex CLI: 5 files, 1 skill in .agents/skills/
  • Cursor: 6 files, 1 skill in .agents/skills/, 1 rule
  • Gemini CLI: 6 files, 1 skill in .agents/skills/
  • Antigravity CLI: 5 files, 1 skill in .agents/skills/
  • GitHub Copilot: 5 files, 1 skill in .agents/skills/
MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. Not applicable No MCP servers in this loadout
Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode, instructions hidden in HTML comments or after long blank padding, or prompt-injection and covert wording (such as telling the agent to keep something from you), and the listing's network flag is honest. Pass
Clean: no credentials, risky shell, hidden text or covert instructions
  • Scanner intake-precheck/2: verdict clean; network yes (listing says yes)
ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. Pass 3 references between files resolve for every agent
Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. Pass
8 files, 18.4 KB total; largest skills/django-6-guard/scripts/django6_guard.py 7.6 KB
  • Limits: 30 files, 32 KB per file, 128 KB total, 500 lines per skill or rule
LicenceA recognised SPDX licence, with the licence text available. Pass MIT, by SetupBay; text in the repository LICENSE
Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. Pass
django6_guard.py passes a small Django 6.1 shop project and flags every old line in the skill's Django 3-era sample: 12 removed APIs as errors and 2 EMAIL_* settings as warnings. django-upgrade 1.33.0 finds nothing to rewrite in the project and would rewrite the sample. The pinned Django 6.1.2 passe
  • Applied 12 loadout files at every agent's paths
  • ✓ scanner: the Django 6.1 project is clean: bash -c python3 .agents/skills/django-6-guard/scripts/django6_guard.py config shop manage.py; echo "exit=$?"
  • ✓ scanner: the Django 3-era sample is flagged line by line: bash -c mkdir -p /tmp/django3-sample && cp .claude/skills/django-6-guard/references/django3-sample.py.txt /tmp/django3-sample/models.py && python3 .claude/skills/django-6-guard/scripts/django6_guard.py /tmp/django3-sample; echo "exit=$?"
  • ✓ django-upgrade 1.33.0: nothing to rewrite in the project, the sample would be rewritten: bash -c uvx -q --from django-upgrade==1.33.0 django-upgrade --check --target-version 6.1 manage.py config/*.py shop/*.py shop/migrations/*.py; echo "project=$?"; cd /tmp/django3-sample && uvx -q --from django-upgrade==1.33.0 django-upgrade --check --target-version 6.1 models.py; echo "sample=$?"
  • ✓ Django 6.1.2: system checks and migrations are clean: bash -c uv run --python 3.12 python manage.py check && uv run --python 3.12 python manage.py makemigrations --check --dry-run
  • ✓ tests with RemovedInDjango70Warning and RemovedInDjango2028Warning as errors: uv run --python 3.12 python -W error::django.utils.deprecation.RemovedInDjango70Warning -W error::django.utils.deprecation.RemovedInDjango2028Warning manage.py test -v 1
  • ✓ Django 6.1.2 refuses the sample's removed calls: uv run --python 3.12 python legacy_probe.py
  • Covers: The scanner's removed-API rules against a real Django 3-era module and settings, and a clean Django 6.1 project (models, services, views, URLs, template, migration)
  • Covers: django-upgrade 1.33.0 --check at target 6.1 on both
  • Covers: Django 6.1.2 itself: system checks, makemigrations --check, the tests with the next deprecations as errors, MAILERS mail, the CSP middleware and FETCH_RAISE
  • Covers: Not covered: Postgres-only aggregates (ArrayAgg ordering=) and login(request, None), which the scanner flags but the SQLite fixture can't run

What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results from agent runs are separate and stay untested until an agent actually runs.

Install

Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.

Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.

Download zip
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/django-6-upgrade-guard/1.0.0/AGENTS.md
curl -fsSL --create-dirs -o CLAUDE.md https://setupbay.com/raw/django-6-upgrade-guard/1.0.0/CLAUDE.md
curl -fsSL --create-dirs -o .claude/skills/django-6-guard/SKILL.md https://setupbay.com/raw/django-6-upgrade-guard/1.0.0/skills/django-6-guard/SKILL.md
curl -fsSL --create-dirs -o .claude/skills/django-6-guard/scripts/django6_guard.py https://setupbay.com/raw/django-6-upgrade-guard/1.0.0/skills/django-6-guard/scripts/django6_guard.py
curl -fsSL --create-dirs -o .claude/skills/django-6-guard/references/removals.md https://setupbay.com/raw/django-6-upgrade-guard/1.0.0/skills/django-6-guard/references/removals.md
curl -fsSL --create-dirs -o .claude/skills/django-6-guard/references/django3-sample.py.txt https://setupbay.com/raw/django-6-upgrade-guard/1.0.0/skills/django-6-guard/references/django3-sample.py.txt
FileGoes to
AGENTS.mdAGENTS.md
CLAUDE.mdCLAUDE.md
skills/django-6-guard/SKILL.md.claude/skills/django-6-guard/SKILL.md
skills/django-6-guard/scripts/django6_guard.py.claude/skills/django-6-guard/scripts/django6_guard.py
skills/django-6-guard/references/removals.md.claude/skills/django-6-guard/references/removals.md
skills/django-6-guard/references/django3-sample.py.txt.claude/skills/django-6-guard/references/django3-sample.py.txt

Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Claude Code reads.

Or as a Claude Code plugin

Adds the skill to Claude Code for all your projects, without copying files. Plugins can't carry project rules, so add AGENTS.md and CLAUDE.md with the commands above. About the plugin marketplace.

/plugin marketplace add https://setupbay.com/marketplace.json
/plugin install django-6-upgrade-guard@setupbay

Files

Rules AGENTS.md Django 6 version facts, rules and the done checklist. 37 lines
Download Raw
# AGENTS.md: Django 6 project (old Django guard)

This project runs **Django 6.1** on Python 3.12 or later. Much of the Django code you remember is from 2.x–4.x,
and Django has removed a lot since then: `url()`, `ugettext`, `force_text`, `is_ajax()`, `index_together`,
`NullBooleanField`, and in 6.0 and 6.1 `CheckConstraint(check=)`, positional `Model.save()` arguments and more.

## Version facts
- Check the pin first: `uv run python -m django --version`.
- Django 6.0 needs Python 3.12+, defaults `DEFAULT_AUTO_FIELD` to `BigAutoField`, and assumes `https` in
  `forms.URLField`. It also adds built-in CSP support, template partials and a background Tasks framework (`django.tasks`).
- Django 6.1 removes the 5.2 deprecations, for example `finders.find(all=)` → `find_all=`, and
  `ArrayAgg(ordering=)` → `order_by=`. `login(request, None)` no longer falls back to `request.user`.
- Django 6.1 deprecates the `EMAIL_*` connection settings: configure mail in `MAILERS`
  (`{"default": {"BACKEND": ..., "OPTIONS": {...}}}`), in the same style as `DATABASES` and `CACHES`.
- Django 6.1 adds fetch modes: `Order.objects.fetch_mode(models.FETCH_RAISE)` turns a lazy load (an N+1) into an
  error, and `FETCH_PEERS` loads the field for every instance from the same query at once.
- Deprecation warnings for the next removals are `RemovedInDjango70Warning`, then `RemovedInDjango2028Warning`
  (the class name says which release removes them).
- If you're not sure an API exists, check the docs for this version (https://docs.djangoproject.com/en/6.1/) and
  the release notes, not old tutorials.

## Rules
- Write Django 6 code only. The `django-6-guard` skill has the full table. The common ones are: `path()` /
  `re_path()`, `gettext_lazy`, `force_str`, `CheckConstraint(condition=)`, `save(update_fields=[...])` with keywords,
  `Meta.indexes`, `BooleanField(null=True)`, `{% load static %}`, `datetime.UTC` / `zoneinfo`.
- Run `django-upgrade --target-version 6.1` on files you change. It rewrites many old forms safely.
- Every model change comes with a migration made by `makemigrations`. Never write or edit migrations by hand to "fix" a check.
- For simple background jobs, look at `django.tasks` before adding Celery, and ask the human.
- Don't change the Django version or `DEFAULT_AUTO_FIELD` in an existing project without asking.

## Before you say "done"
1. Run the `django-6-guard` scanner on the apps and templates you touched. It must report `0 error(s)`.
2. Run `django-upgrade --check --target-version 6.1` on the changed `.py` files. It must report nothing to rewrite.
3. Run `python manage.py check` and `python manage.py makemigrations --check --dry-run`.
4. Run the tests with next-version deprecations as errors:
   `python -W error::django.utils.deprecation.RemovedInDjango70Warning -W error::django.utils.deprecation.RemovedInDjango2028Warning manage.py test`.
Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
Download Raw
# CLAUDE.md

@AGENTS.md

## Claude Code notes

- Skills from this loadout live in `.claude/skills/`: `django-6-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From SetupBay "Django 6 guard for removed APIs and django-upgrade" v1.0.0 — https://setupbay.com/loadouts/django-6-upgrade-guard/ -->
Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
Download Raw
# GEMINI.md

@./AGENTS.md

## Gemini CLI notes

- Skills from this loadout live in `.agents/skills/`: `django-6-guard`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.

<!-- From SetupBay "Django 6 guard for removed APIs and django-upgrade" v1.0.0 — https://setupbay.com/loadouts/django-6-upgrade-guard/ -->
Cursor rule .cursor/rules/django-6-only.mdc Never/always table for Python files and templates. 25 lines
Download Raw
---
description: Django 6.1 only; no APIs removed in Django 4.0 to 6.1
globs: "**/*.py,**/templates/**/*.html"
alwaysApply: false
---

This project runs Django 6.1 (Python 3.12+). Never write removed Django APIs.

| Never | Always (Django 6.1) |
|---|---|
| `from django.conf.urls import url`; `url(r"^x/$", v)` | `path("x/", v)` or `re_path(r"^x/$", v)` |
| `ugettext`, `ugettext_lazy` | `gettext`, `gettext_lazy` |
| `force_text`, `smart_text` | `force_str`, `smart_str` |
| `request.is_ajax()` | check the `Accept` or `X-Requested-With` header yourself |
| `Meta.index_together` | `Meta.indexes = [models.Index(fields=[...])]` |
| `models.NullBooleanField()` | `models.BooleanField(null=True)` |
| `CheckConstraint(check=Q(...))` | `CheckConstraint(condition=Q(...))` |
| `obj.save(False, False)` | `obj.save(update_fields=[...])` (keywords only) |
| `format_html("<b>fixed</b>")` | `mark_safe(...)`, or `format_html("<b>{}</b>", value)` |
| `finders.find(p, all=True)` | `finders.find(p, find_all=True)` |
| `timezone.utc`, `pytz` | `datetime.UTC`, `zoneinfo.ZoneInfo` |
| `{% load staticfiles %}` | `{% load static %}` |

- After editing, run the `django-6-guard` skill's scanner, `django-upgrade --check --target-version 6.1`, `manage.py check` and the tests.
Skill skills/django-6-guard/SKILL.md Scan for removed Django APIs, run django-upgrade, then Django's checks and the tests with deprecations as errors. 49 lines
Download Raw
---
name: django-6-guard
description: Check a Django project for APIs removed between Django 4.0 and 6.1, run django-upgrade and Django's own checks, before saying a Django change is done. Use after writing or editing models, views, URLs, settings or templates, when Django raises an unexpected keyword or missing attribute error, or when porting older Django snippets or tutorials.
---

# Django 6 guard

Agents trained on years of Django answers still write `url()`, `ugettext_lazy`, `force_text`, `request.is_ajax()`,
`index_together` and `{% load staticfiles %}`. All of those are gone. Django 6.0 and 6.1 removed more, including
`CheckConstraint(check=)`, positional `Model.save()` arguments, `format_html()` without arguments,
`finders.find(all=)` and the Postgres aggregates' `ordering=`. This skill finds them, lets `django-upgrade` rewrite
what it can, then lets Django check the rest.

## 1. Confirm the version
- `uv run python -m django --version` should print `6.1.x`. If it doesn't, ask before changing code for a different version.

## 2. Scan
```bash
python3 .agents/skills/django-6-guard/scripts/django6_guard.py shop templates
```
(Use the path the skill was installed to: `.claude/skills/...` for Claude Code, `.agents/skills/...` for the others.)

- Every `error` line is a removed API. Fix it with the replacement on the line. `references/removals.md` says which
  release removed each one.
- `warn` lines are behaviour changes to decide on: `login(request, None)`, an explicit `AutoField` default,
  positional mail arguments, `EMAIL_*` settings that now belong in `MAILERS`, or Celery added where `django.tasks` might do.

## 3. Let django-upgrade rewrite what it safely can
```bash
uvx --from django-upgrade==1.33.0 django-upgrade --target-version 6.1 shop/*.py
```
- Then run it again with `--check`. It must print nothing. Review its diff like any other change.
- It doesn't cover everything the scanner does (for example `NullBooleanField` or `CheckConstraint(check=)`),
  so run both.

## 4. Let Django check it
```bash
python manage.py check
python manage.py makemigrations --check --dry-run
python -W error::django.utils.deprecation.RemovedInDjango70Warning \
       -W error::django.utils.deprecation.RemovedInDjango2028Warning manage.py test
```
- `check` reports removed fields as system-check errors (for example `fields.E903` for `NullBooleanField`).
- If `makemigrations --check` wants a migration, create it with `makemigrations` and include it in the change.

## 5. Report
Say which removed APIs you replaced, what django-upgrade rewrote, any migration you added, and that the scanner,
`check`, `makemigrations --check` and the tests were clean.
Skill file skills/django-6-guard/scripts/django6_guard.py Read-only scanner for removed Django APIs and Django 6 behaviour changes. 156 lines
Download Raw
#!/usr/bin/env python3
"""Find Django code removed by 6.0 and 6.1 (and older removals agents still write) in a Django 6 project.

Usage: python3 django6_guard.py [PATH ...]        (default: the current folder)
       python3 django6_guard.py --list            (print every rule)

A "guard: ok" comment on a line silences its warnings (never its errors).
Prints one line per finding:  path:line: level [rule] what is wrong -> what to use
Exit 0 when nothing at "error" level is found, 1 when something is, 2 on bad usage.
Standard library only. Read-only: it never edits a file and makes no network calls.
"""
import fnmatch
import os
import re
import sys

# (rule id, level, regex, message, file patterns). Patterns run against each line with comments removed.
RULES = [
    ('check-constraint-check', 'error', 'CheckConstraint\\([^)]*\\bcheck\\s*=',
     'CheckConstraint(check=...) was removed in 6.0 -> CheckConstraint(condition=...)', ['*.py']),
    ('save-positional', 'error', '\\.save\\(\\s*(True|False)\\s*,',
     'Model.save() positional arguments were removed in 6.0 -> save(force_insert=..., force_update=...)', ['*.py']),
    ('format-html-no-args', 'error', 'format_html\\(\\s*[frbu]*(\\"[^\\"]*\\"|\'[^\']*\')\\s*\\)',
     'format_html() with no args or kwargs raises in 6.0 -> mark_safe() for fixed HTML, or pass the values as arguments', ['*.py']),
    ('staticfiles-find-all', 'error', 'finders\\.find\\([^)]*\\ball\\s*=',
     'finders.find(all=...) was removed in 6.1 -> find(..., find_all=True)', ['*.py']),
    ('pg-agg-ordering', 'error', '\\b(ArrayAgg|JSONBAgg|StringAgg)\\([^)]*\\bordering\\s*=',
     'ArrayAgg/JSONBAgg/StringAgg(ordering=...) was removed in 6.1 -> order_by=...', ['*.py']),
    ('choices-meta', 'error', '\\bChoicesMeta\\b',
     'ChoicesMeta was removed in 6.0 -> ChoicesType', ['*.py']),
    ('urlfield-assume', 'error', 'FORMS_URLFIELD_ASSUME_HTTPS',
     'FORMS_URLFIELD_ASSUME_HTTPS was removed; URLField now assumes https', ['*.py']),
    ('log-action', 'error', '\\.log_action\\(|\\.log_deletion\\(',
     'LogEntryManager.log_action() / ModelAdmin.log_deletion() were removed in 6.0 -> log_actions() / log_deletions()', ['*.py']),
    ('index-together', 'error', '\\bindex_together\\s*=',
     'Meta.index_together was removed in 5.1 -> indexes = [models.Index(fields=[...])]', ['*.py']),
    ('url-function', 'error', 'from\\s+django\\.conf\\.urls\\s+import\\s+[^#]*\\burl\\b|\\burl\\(\\s*r?[\\"\']\\^',
     'django.conf.urls.url() was removed in 4.0 -> path() or re_path()', ['*.py']),
    ('ugettext', 'error', '\\bu(gettext|gettext_lazy|ngettext|ngettext_lazy|pgettext)\\b',
     'ugettext* was removed in 4.0 -> gettext / gettext_lazy', ['*.py']),
    ('force-text', 'error', '\\b(force_text|smart_text)\\b',
     'force_text / smart_text were removed in 4.0 -> force_str / smart_str', ['*.py']),
    ('is-ajax', 'error', '\\.is_ajax\\(\\)',
     "request.is_ajax() was removed in 4.0 -> check request.headers.get('x-requested-with') or the Accept header", ['*.py']),
    ('timezone-utc', 'error', 'timezone\\.utc\\b(?!c)|import\\s+pytz|pytz\\.',
     'django.utils.timezone.utc and pytz support were removed -> datetime.UTC / zoneinfo.ZoneInfo', ['*.py']),
    ('use-l10n', 'error', '^\\s*USE_L10N\\s*=',
     'USE_L10N was removed in 5.0 (localisation is always on); delete the setting', ['*.py']),
    ('null-boolean', 'error', '\\bNullBooleanField\\(',
     'NullBooleanField is gone (system check fields.E903) -> BooleanField(null=True)', ['*.py']),
    ('staticfiles-tag', 'error', '\\{%\\s*load\\s+(staticfiles|admin_static)\\s*%\\}',
     '{% load staticfiles %} was removed -> {% load static %}', ['*.html', '*.txt']),
    ('login-user-none', 'warn', '\\b(a?login)\\(\\s*request\\s*,\\s*None\\b',
     'login(request, None) no longer falls back to request.user in 6.1 -> pass the user', ['*.py']),
    ('default-auto-field', 'warn', 'DEFAULT_AUTO_FIELD\\s*=\\s*[\\"\']django\\.db\\.models\\.AutoField[\\"\']',
     "Django 6.0 defaults to BigAutoField; keeping AutoField is fine for old apps, but new apps shouldn't set it", ['*.py']),
    ('mail-positional', 'warn', '\\b(send_mail|send_mass_mail|mail_admins|mail_managers|EmailMessage)\\(\\s*[^)=]+,\\s*[^)=]+,\\s*[^)=]+,\\s*[^)=]+,\\s*[^)=]+[,)]',
     'positional arguments past the first four in django.core.mail are deprecated in 6.0 -> keywords', ['*.py']),
    ('email-settings', 'warn', '^\\s*EMAIL_(BACKEND|HOST|HOST_USER|HOST_PASSWORD|PORT|USE_TLS|USE_SSL|TIMEOUT|SSL_CERTFILE|SSL_KEYFILE|FILE_PATH)\\s*=',
     'the EMAIL_* connection settings are deprecated in 6.1 (RemovedInDjango2028Warning) -> MAILERS = {"default": {"BACKEND": ..., "OPTIONS": {...}}}', ['*.py']),
    ('celery-for-simple-task', 'warn', '@shared_task\\b|from\\s+celery\\s+import',
     'Django 6.0 ships a Tasks framework (django.tasks); ask before adding Celery for simple background jobs', ['*.py']),
]
FILES = ['*.py', '*.html', '*.txt']
COMMENT = '#'
SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".agents", ".claude", ".cursor", "dist", "build", "target", ".godot"}
COMPILED = [(rid, lvl, re.compile(rx), msg, globs) for rid, lvl, rx, msg, globs in RULES]


def strip_comment(line):
    """Drop a trailing comment, leaving string literals alone."""
    q = None
    i = 0
    while i < len(line):
        c = line[i]
        if q:
            if c == "\\":
                i += 2
                continue
            if c == q:
                q = None
        elif c in "\"'`":
            q = c
        elif line.startswith(COMMENT, i):
            return line[:i]
        i += 1
    return line


def wanted(name):
    return any(fnmatch.fnmatch(name, g) for g in FILES)


def walk(paths):
    for p in paths:
        if os.path.isfile(p):
            yield p
            continue
        for root, dirs, files in os.walk(p):
            dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
            for f in sorted(files):
                if wanted(f):
                    yield os.path.join(root, f)


def scan(path):
    out = []
    name = os.path.basename(path)
    try:
        with open(path, encoding="utf-8", errors="replace") as fh:
            lines = fh.read().splitlines()
    except OSError as e:
        return [(path, 0, "error", "unreadable", str(e))]
    for n, raw in enumerate(lines, 1):
        code = strip_comment(raw)
        if not code.strip():
            continue
        accepted = "guard: ok" in raw  # a person accepted the warnings on this line
        for rid, lvl, rx, msg, globs in COMPILED:
            if accepted and lvl == "warn":
                continue
            if globs and not any(fnmatch.fnmatch(name, g) for g in globs):
                continue
            if rx.search(code):
                out.append((path, n, lvl, rid, msg))
    return out


def main(argv):
    if "--list" in argv:
        for rid, lvl, _, msg, globs in RULES:
            print(f"{lvl:5} [{rid}] {msg}" + (f"  (only {', '.join(globs)})" if globs else ""))
        return 0
    if any(a.startswith("-") for a in argv):
        print(__doc__)
        return 2
    paths = argv or ["."]
    for p in paths:
        if not os.path.exists(p):
            print(f"no such path: {p}", file=sys.stderr)
            return 2
    files = list(walk(paths))
    findings = []
    for f in files:
        findings.extend(scan(f))
    for path, n, lvl, rid, msg in findings:
        print(f"{os.path.relpath(path)}:{n}: {lvl} [{rid}] {msg}")
    errors = sum(1 for x in findings if x[2] == "error")
    warns = sum(1 for x in findings if x[2] == "warn")
    print(f"django6_guard: {len(files)} file(s), {errors} error(s), {warns} warning(s)")
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
Skill file skills/django-6-guard/references/removals.md Which release removed each API and what replaces it. 35 lines
Download Raw
# Django removals agents still write

From the Django release notes (https://docs.djangoproject.com/en/6.1/releases/). Checked against Django 6.1.2.

| Old code | Removed in | Use |
|---|---|---|
| `from django.conf.urls import url` | 4.0 | `from django.urls import path, re_path` |
| `ugettext`, `ugettext_lazy`, `ungettext` | 4.0 | `gettext`, `gettext_lazy`, `ngettext` |
| `force_text`, `smart_text` | 4.0 | `force_str`, `smart_str` |
| `request.is_ajax()` | 4.0 | `request.headers.get("x-requested-with") == "XMLHttpRequest"`, or better, the `Accept` header |
| `NullBooleanField()` | 4.0 (model field kept only for old migrations; system check `fields.E903`) | `BooleanField(null=True)` |
| `django.utils.timezone.utc`, pytz support | 5.0 | `datetime.UTC`, `zoneinfo.ZoneInfo` |
| `USE_L10N = True` | 5.0 | delete it; always on |
| `Meta.index_together` | 5.1 | `Meta.indexes = [models.Index(fields=["a", "b"])]` |
| `CheckConstraint(check=...)` | 6.0 | `CheckConstraint(condition=...)` |
| `obj.save(False, False, None, None)` | 6.0 | `obj.save(force_insert=..., update_fields=[...])` |
| `format_html("<b>fixed</b>")` with no args | 6.0 | `mark_safe("<b>fixed</b>")` or `format_html("<b>{}</b>", v)` |
| `ChoicesMeta` | 6.0 | `ChoicesType` |
| `ModelAdmin.log_deletion()`, `LogEntryManager.log_action()` | 6.0 | `log_deletions()`, `log_actions()` |
| `FORMS_URLFIELD_ASSUME_HTTPS` | 6.0 | delete it; `URLField` assumes `https` |
| `finders.find(path, all=True)` | 6.1 | `finders.find(path, find_all=True)` |
| `ArrayAgg("x", ordering=...)` (also JSONBAgg, StringAgg) | 6.1 | `order_by=...` |
| `login(request, None)` falling back to `request.user` | 6.1 | pass the user explicitly |
| `{% load staticfiles %}`, `{% load admin_static %}` | 3.0 | `{% load static %}` |

## New in 6.0 worth knowing
- `DEFAULT_AUTO_FIELD` defaults to `django.db.models.BigAutoField`.
- Python 3.12 or later only.
- Built-in Content Security Policy support (`django.middleware.csp.ContentSecurityPolicyMiddleware`, `SECURE_CSP`).
- Template partials (`{% partialdef %}` / `{% partial %}`).
- A background Tasks framework (`django.tasks`), which needs a backend to actually run tasks.
- Django 6.1: mail is configured in `MAILERS`; the `EMAIL_*` connection settings warn with
  `RemovedInDjango2028Warning`.
- Django 6.1: fetch modes (`QuerySet.fetch_mode(models.FETCH_RAISE)`, `FETCH_PEERS`, `FETCH_ONE`).
Skill file skills/django-6-guard/references/django3-sample.py.txt A Django 3-era module and settings for checking the guard. 42 lines
Download Raw
# A Django 3.x-era module, used to check the guard. Every marked API is gone in Django 6.1.
from django.conf.urls import url
from django.db import models
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.translation import ugettext_lazy as _


class Order(models.Model):
    total = models.IntegerField()
    paid = models.NullBooleanField()
    code = models.CharField(max_length=8)

    class Meta:
        index_together = [["code", "total"]]
        constraints = [models.CheckConstraint(check=models.Q(total__gte=0), name="total_gte_0")]

    def __str__(self):
        return force_text(self.code)

    def badge(self):
        return format_html("<span class='paid'>paid</span>")


def mark_paid(order):
    order.paid = True
    order.save(False, False)


def view(request):
    if request.is_ajax():
        return None


urlpatterns = [url(r"^orders/$", view)]


# --- settings.py from the same project ---
USE_L10N = True
EMAIL_HOST = "smtp.example.com"
EMAIL_PORT = 587

All files: source zip · Licence MIT · By SetupBay · First listed

Installing and checks

How do I install Django 6 guard for removed APIs and django-upgrade in Claude Code?

From your project root, put AGENTS.md, CLAUDE.md, skills/django-6-guard/SKILL.md at .claude/skills/django-6-guard/SKILL.md, skills/django-6-guard/scripts/django6_guard.py at .claude/skills/django-6-guard/scripts/django6_guard.py, skills/django-6-guard/references/removals.md at .claude/skills/django-6-guard/references/removals.md, skills/django-6-guard/references/django3-sample.py.txt at .claude/skills/django-6-guard/references/django3-sample.py.txt. The "Claude Code" tab on this page has the curl commands, or download https://setupbay.com/download/django-6-upgrade-guard/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Django 6 guard for removed APIs and django-upgrade in Codex CLI?

From your project root, put AGENTS.md, skills/django-6-guard/SKILL.md at .agents/skills/django-6-guard/SKILL.md, skills/django-6-guard/scripts/django6_guard.py at .agents/skills/django-6-guard/scripts/django6_guard.py, skills/django-6-guard/references/removals.md at .agents/skills/django-6-guard/references/removals.md, skills/django-6-guard/references/django3-sample.py.txt at .agents/skills/django-6-guard/references/django3-sample.py.txt. The "Codex CLI" tab on this page has the curl commands, or download https://setupbay.com/download/django-6-upgrade-guard/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Django 6 guard for removed APIs and django-upgrade in Cursor?

From your project root, put AGENTS.md, .cursor/rules/django-6-only.mdc, skills/django-6-guard/SKILL.md at .agents/skills/django-6-guard/SKILL.md, skills/django-6-guard/scripts/django6_guard.py at .agents/skills/django-6-guard/scripts/django6_guard.py, skills/django-6-guard/references/removals.md at .agents/skills/django-6-guard/references/removals.md, skills/django-6-guard/references/django3-sample.py.txt at .agents/skills/django-6-guard/references/django3-sample.py.txt. The "Cursor" tab on this page has the curl commands, or download https://setupbay.com/download/django-6-upgrade-guard/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Django 6 guard for removed APIs and django-upgrade in Gemini CLI?

From your project root, put AGENTS.md, GEMINI.md, skills/django-6-guard/SKILL.md at .agents/skills/django-6-guard/SKILL.md, skills/django-6-guard/scripts/django6_guard.py at .agents/skills/django-6-guard/scripts/django6_guard.py, skills/django-6-guard/references/removals.md at .agents/skills/django-6-guard/references/removals.md, skills/django-6-guard/references/django3-sample.py.txt at .agents/skills/django-6-guard/references/django3-sample.py.txt. The "Gemini CLI" tab on this page has the curl commands, or download https://setupbay.com/download/django-6-upgrade-guard/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Django 6 guard for removed APIs and django-upgrade in Antigravity CLI?

From your project root, put AGENTS.md, skills/django-6-guard/SKILL.md at .agents/skills/django-6-guard/SKILL.md, skills/django-6-guard/scripts/django6_guard.py at .agents/skills/django-6-guard/scripts/django6_guard.py, skills/django-6-guard/references/removals.md at .agents/skills/django-6-guard/references/removals.md, skills/django-6-guard/references/django3-sample.py.txt at .agents/skills/django-6-guard/references/django3-sample.py.txt. The "Antigravity CLI" tab on this page has the curl commands, or download https://setupbay.com/download/django-6-upgrade-guard/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

How do I install Django 6 guard for removed APIs and django-upgrade in GitHub Copilot?

From your project root, put AGENTS.md, skills/django-6-guard/SKILL.md at .agents/skills/django-6-guard/SKILL.md, skills/django-6-guard/scripts/django6_guard.py at .agents/skills/django-6-guard/scripts/django6_guard.py, skills/django-6-guard/references/removals.md at .agents/skills/django-6-guard/references/removals.md, skills/django-6-guard/references/django3-sample.py.txt at .agents/skills/django-6-guard/references/django3-sample.py.txt. The "GitHub Copilot" tab on this page has the curl commands, or download https://setupbay.com/download/django-6-upgrade-guard/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.

What has been checked in Django 6 guard for removed APIs and django-upgrade?

It passed the Level 1 automated install checks on 2026-10-29: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.

More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.