Error codes¶
Every error Karni raises starts with a code and ends with a link to its entry on this page:
E104: EventViewSet: Owned('organiser') does not lead from Event to the user model
(Event has no field 'organiser'). Name a relation path ending at the user, e.g.
Owned("organizer") or Owned("event__organizer"). See …/errors/#e104
The message names your class, field or setting. The entry below says why Karni objects and how to fix it in general. Offline, without a browser:
Codes are permanent: a code never changes meaning and is never reused, so match on the code, not on the wording, which improves between releases.
| Prefix | Comes from |
|---|---|
E / W |
Karni itself: exceptions, system checks (W = warning), API errors, test failures |
PAY |
the karni-payments module (each module has its own prefix) |
C |
the karni command |
K |
agent rules, enforced by karni check and the architecture tests |
karni/… |
the ESLint plugin shipped with @karni/ui |
System checks run on runserver, migrate, check and every management
command, so problems surface on boot rather than on the first request. Their
ids are karni.<code> (silence a warning on purpose with
SILENCED_SYSTEM_CHECKS = ["karni.W001"]).
End-user outcomes are not on this page: wrong credentials, expired or used
tokens, an invalid 2FA code, a declined payment, a 404 for a row outside your
access. Those are ordinary API responses (see accounts and
api), not mistakes in your code.
Karni (E, W)¶
0xx · Models, managers and scopes¶
E001 — A soft-delete model's default manager shows deleted rows¶
system check (error) · from karni.checks.check_soft_delete_managers
Why Karni objects. The model inherits SoftDeleteModel, but its default manager does not run Karni's scope pipeline, usually because another mixin listed first supplied a plain objects. Deleted rows then appear in every query, relation and API response, and the model misses every future scope such as tenant.
Fix. Inherit karni.models.BaseModel, or list SoftDeleteModel first among the Karni mixins, or declare objects = SoftDeleteManager() (any manager built on karni.managers.ScopedManagerMixin) with class Meta(SoftDeleteModel.Meta).
W001 — A registered scope is switched off by KARNI_SCOPES¶
system check (warning) · from karni.checks.check_scopes
Why Karni objects. KARNI_SCOPES lists the scopes that run; a registered scope it leaves out filters nothing, which is usually an accident after installing a module.
Fix. Add the scope's name to KARNI_SCOPES (or unset KARNI_SCOPES to run every scope). If switching it off is deliberate, add 'karni.W001' to SILENCED_SYSTEM_CHECKS.
E002 — A scope or the KARNI_SCOPES setting is invalid¶
system check (error) · from karni.scopes registry; karni.checks.check_scopes
Why Karni objects. Every query of a scoped model runs the registered scopes in order. A scope that fails to load, has a bad name or order, or a KARNI_SCOPES list that names unknown or duplicate scopes would silently change which rows every query returns.
Fix. Fix the scope class or the KARNI_SCOPES entry the message names: a Scope subclass with an identifier name and an int order; KARNI_SCOPES a list of registered names in pipeline order (or unset to run all of them).
E003 — objects.without() names no scope, or an unknown one¶
exception · from karni.scopes; Model.objects.without()
Why Karni objects. without() lifts a scope by name for one query. An unknown name would lift nothing while the code reads as if it did.
Fix. Pass a registered scope name, e.g. Model.objects.without("soft_delete", reason="..."). The message lists the registered scopes.
E004 — objects.without() needs a reason¶
exception · from karni.scopes; Model.objects.without()
Why Karni objects. Lifting a scope shows rows the scope exists to hide (deleted rows, another tenant's rows). The reason is logged so every such query can be audited.
Fix. Pass reason="
E005 — without() was called on a queryset¶
exception · from karni.managers.ScopedQuerySet.without
Why Karni objects. Scopes are applied when the manager builds the queryset, so a queryset has already filtered by every scope; lifting one there cannot bring rows back.
Fix. Start from the manager: Model.objects.without("
E006 — A scope's context is not bound¶
exception · from karni.scopes.ScopeContextMissing (raised by a scope, e.g. tenant)
Why Karni objects. A scope that needs context (the current tenant) has none, typically in a Celery task, management command or test. Karni fails closed instead of returning every tenant's rows.
Fix. Bind the context before querying, as the message says (e.g. wrap the task body in the module's context manager), or lift the scope deliberately with Model.objects.without("
E007 — Karni.audit or Karni.sensitive is malformed¶
exception · from karni.audit.capture
Why Karni objects. Change history is opted into per model as True or a list of that model's field names, and sensitive fields are named the same way; a name the model does not have would audit or redact nothing silently.
Fix. Write class Karni: audit = True (or a list of field names) and sensitive = [...] with field names of the model.
1xx · API: access, viewsets, serializers, pagination, budgets, expand¶
E101 — A viewset declares no access¶
system check (error) · from karni.checks.check_viewsets; karni.api.access.KarniAccessPermission
Why Karni objects. Access is mandatory (ADR 0001 Q2.1): without a declaration Karni cannot know which rows a user may see or change, so the viewset denies every request.
Fix. Declare access = Granted() on the viewset (the roles' grants for its model, row-scoped), or a custom Access(read=..., write=...) (e.g. Access(read=Authenticated(), write=Owned('owner'))), or opt out explicitly with access = Unscoped(reason='...').
W101 — A hand-written serializer duplicates the one Karni generates¶
system check (warning) · from karni.checks.check_viewsets
Why Karni objects. Karni generates the serializer from the model (APIMeta tunes it). A copy that declares nothing of its own drifts from the model and from the schema the frontend is generated from.
Fix. Delete the serializer and its serializer_class line, and tune the generated one with APIMeta on the model. Keep serializer_class only for a serializer with custom fields or validation.
E102 — access is not an Access(...) or Unscoped(...) declaration¶
exception · from karni.api.access.validate_access
Why Karni objects. The access declaration compiles into both the request gate and the row filter; anything else would leave one of them undefined.
Fix. Write access = Access(read=..., write=..., create=...) with predicates, or access = Unscoped(reason='...'). A bare predicate goes inside Access(read=...).
E103 — A predicate was expected but something else was given¶
exception · from karni.api.access: Access(...), Owned(bypass=...), @action(access=...)
Why Karni objects. Access slots, Owned(bypass=...) and @action(access=...) take predicate instances so they can be evaluated per request and per row.
Fix. Pass a predicate instance: Owned('field'), HasRole('staff'), Authenticated(), Public(), Where(fn), combined with | and &. Write HasRole('staff'), not HasRole.
E104 — Owned() does not name a path to the user¶
exception · from karni.api.access.Owned; karni.api.access.validate_access
Why Karni objects. Owned('path') filters rows to those whose path leads to request.user, and stamps the owner on create; a path that does not end at the user model would match nothing or the wrong rows.
Fix. Name the owner relation as a string, following relations with __ and ending at the user model: Owned('organizer') or Owned('event__organizer').
E105 — HasRole() names no roles¶
exception · from karni.api.access.HasRole
Why Karni objects. Roles are Django group names; a HasRole without names would grant nobody.
Fix. Pass one or more group names: HasRole('staff') or HasRole('organizer', 'staff').
E106 — A Where() function is invalid¶
exception · from karni.api.access.Where
Why Karni objects. Where(fn) calls fn(request) for the request gate and as the row filter; it must be callable and return a Q (rows) or a bool (everything or nothing).
Fix. Pass a function of the request returning a Q or a bool, e.g. Where(lambda request: Q(published=True)).
E107 — Unscoped() needs a reason¶
exception · from karni.api.access.Unscoped
Why Karni objects. Opting a viewset out of access control is sometimes right (a public catalogue), but it must be deliberate and reviewable; the reason is published in karni context and the schema.
Fix. Write access = Unscoped(reason='public catalogue') with a non-empty reason.
E109 — Access predicates were combined with and / or / not¶
exception · from karni.api.access.Predicate.__bool__
Why Karni objects. Python's and/or/not evaluate truthiness, so one side of the expression would be silently dropped.
Fix. Combine predicates with | and &: Owned('user') | HasRole('staff').
E110 — A viewset has neither model nor queryset¶
exception · from karni.api.viewsets.BaseViewSet
Why Karni objects. Karni generates the queryset, serializer, filters and expansion from the model.
Fix. Set model = YourModel on the viewset (or queryset = ...), or override get_queryset() and get_serializer_class().
E111 — Pagination was switched off without Unpaginated(...)¶
exception · from karni.pagination.check_pagination_class; BaseViewSet
Why Karni objects. An unpaginated list grows with the table until it times out (ADR 0001 Q3.5), so every list must be bounded.
Fix. Keep the default paginator, or declare pagination = Unpaginated(max_rows=N, reason='...') to return a bounded plain list.
E112 — pagination_class is not a Karni paginator¶
exception · from karni.pagination.check_pagination_class
Why Karni objects. Karni's paginators enforce KARNI_MAX_PAGE_SIZE; another paginator would let a client request unbounded pages.
Fix. Subclass karni.pagination.KarniPageNumberPagination or KarniCursorPagination.
E113 — Unpaginated(...) is invalid¶
exception · from karni.pagination.Unpaginated; BaseViewSet.pagination
Why Karni objects. Unpaginated lists still need a hard row limit and a reviewable reason.
Fix. Write pagination = Unpaginated(max_rows=500, reason='CSV export') with a positive max_rows and a non-empty reason. To change the paginator, set pagination_class instead.
E114 — Both pagination and pagination_class are declared¶
exception · from karni.api.viewsets.BaseViewSet
Why Karni objects. The two declarations contradict each other.
Fix. Keep one: pagination = Unpaginated(...) for a plain bounded list, or pagination_class for another Karni paginator.
E115 — A query Budget(...) is invalid¶
exception · from karni.performance.Budget; BaseViewSet.query_budget
Why Karni objects. A query budget caps SQL statements per request to catch N+1 regressions; raising it must be a measured, explained decision.
Fix. Write query_budget = Budget(list=25, reason='...'): name at least one action, use positive integers (None stops checking an action) and give a non-empty reason.
E116 — query_budget names unknown actions¶
exception · from karni.api.viewsets.BaseViewSet
Why Karni objects. A budget for an action the viewset does not have would never apply.
Fix. Use the viewset's action names (list, retrieve, create, update, partial_update, destroy, or an @action's name), or default= for every other action.
E117 — A request ran more SQL queries than its budget¶
exception · from karni.api.viewsets.BaseViewSet; karni.testing.query_budget
Why Karni objects. Repeated statements are almost always an N+1: a relation loaded once per row. Budgets turn that into a failing test instead of a slow production page.
Fix. Read the repeated statements in the message and load that relation up front (select_related / prefetch_related, or let the optimizer see it through the serializer). If the work is real, raise the limit: query_budget = Budget(list=N, reason='...').
E118 — KARNI_QUERY_BUDGET_MODE is invalid¶
exception · from karni.performance.budget_mode; karni.testing.pytest_plugin
Why Karni objects. The mode decides whether an exceeded budget is ignored, logged or raised.
Fix. Set KARNI_QUERY_BUDGET_MODE (or the pytest ini option karni_query_budget_mode) to 'off', 'warn' or 'raise'.
E119 — Cache(...) or Uncached(...) is invalid¶
exception · from karni.api.caching; BaseViewSet.cache
Why Karni objects. Response caching must have a positive lifetime, and opting a viewset out must be explained.
Fix. Write cache = Cache(ttl=300) with a positive integer, or cache = Uncached(reason='balances change on every request').
E120 — Query hints are invalid¶
exception · from karni.api.optimizer.validate_hints
Why Karni objects. select_related / prefetch_related / select hints are merged into the optimizer's plan; they must be lookups it can apply.
Fix. Give a list of lookup strings (e.g. select_related = ["venue"]) or Prefetch objects for prefetch_related.
E121 — optimize must be True or False¶
exception · from karni.api.viewsets.BaseViewSet
Why Karni objects. optimize switches the automatic query optimizer for the viewset.
Fix. Set optimize = True (default) or optimize = False.
E122 — APIMeta declares both fields and exclude¶
exception · from karni.api.serializers (generated serializers)
Why Karni objects. The generated serializer can take an allow-list or a deny-list, not both.
Fix. Keep APIMeta.fields or APIMeta.exclude on the model.
E123 — APIMeta names fields the serializer does not have¶
exception · from karni.api.serializers (generated serializers)
Why Karni objects. read_only_fields, write_only_fields and expand entries must name fields that the generated serializer exposes, or they would silently do nothing.
Fix. Fix the spelling, or add the field to APIMeta.fields (or remove it from exclude).
E124 — A field is both read-only and write-only¶
exception · from karni.api.serializers (generated serializers)
Why Karni objects. A field cannot be both returned-only and accepted-only.
Fix. Remove the field from APIMeta.read_only_fields or APIMeta.write_only_fields.
E125 — An expandable field is not a relation¶
exception · from karni.api.serializers.expanded_field
Why Karni objects. ?expand= replaces a foreign key's id with the related object; only relations can be expanded.
Fix. Name a ForeignKey, OneToOneField or ManyToManyField (or a reverse relation) of the model.
E126 — The ?expand= parameter names an unknown or invalid path¶
API 400 response · from karni.api.expansion
Why Karni objects. Expansion is validated against the model so a typo returns a 400 instead of a silently unexpanded response.
Fix. Use the keys the error lists (comma-separated, nested with dots: ?expand=venue,tiers.event). Each segment must be a relation.
E127 — The ?expand= path is too deep¶
API 400 response · from karni.api.expansion
Why Karni objects. Each level of expansion multiplies the work per row; the depth is capped by KARNI_API_MAX_EXPAND_DEPTH.
Fix. Request fewer levels, or raise KARNI_API_MAX_EXPAND_DEPTH in settings if the client really needs them.
E128 — KARNI_API_ROUTER cannot be imported¶
system check (error) · from karni.checks.check_viewsets
Why Karni objects. The router factory builds every API route; without it the API serves nothing.
Fix. Point KARNI_API_ROUTER at an importable zero-argument factory returning a router (default 'karni.api.routers.get_default_router').
E129 — List or retrieve queries grow with the number of rows (N+1)¶
test failure · from karni.testing.performance (pytest --pyargs karni.testing.performance)
Why Karni objects. The scaling check runs list and retrieve with few and many rows; more rows must not mean more queries, or the page slows down as the table grows.
Fix. Let the optimiser see the relation through the serializer, or add select_related / prefetch_related hints (or APIMeta.select/prefetch) for a SerializerMethodField or property. Opt out only with # karni: allow(query-scaling): <reason> above the viewset class.
E130 — An opt-out marker has no reason¶
test failure · from karni.testing (# karni: allow(<rule>) markers)
Why Karni objects. Opting out of a check must be deliberate and reviewable; the reason is the review.
Fix. Write the reason after the colon: # karni: allow(<rule>): <why>.
E131 — search_fields names a write-only field¶
system check (error) · from karni.checks.check_viewsets
Why Karni objects. A field in APIMeta.write_only never appears in a response, but ?search= over it answers whether a row contains a value — a membership oracle on a hidden column.
Fix. Remove the field from search_fields, or take it out of APIMeta.write_only if it may be read.
2xx · Accounts¶
E201 — KARNI_ACCOUNTS_EMAIL_BACKEND is not an accounts email backend¶
exception · from karni.accounts.emails.get_backend
Why Karni objects. Account emails (verification, password reset) are sent through this backend.
Fix. Point the setting at a subclass of karni.accounts.emails.AccountsEmailBackend, e.g. 'karni.accounts.emails.DjangoEmailBackend'.
E202 — No KARNI_ACCOUNTS_LINKS entry for a token purpose¶
exception · from karni.accounts.emails
Why Karni objects. Emails link to your frontend; Karni needs the URL template for each purpose (verify_email, reset_password, ...).
Fix. Add the purpose to KARNI_ACCOUNTS_LINKS, e.g. {'reset_password': '{frontend}/reset-password?token={token}'}.
E203 — create_user() or create_superuser() was called with invalid arguments¶
exception · from karni.accounts.managers.UserManager
Why Karni objects. Karni users log in by email, and a superuser must be staff and superuser.
Fix. Pass email= (and password=). For create_superuser(), leave is_staff and is_superuser unset or True.
E204 — Unknown one-time token purpose¶
exception · from karni.accounts.tokens
Why Karni objects. One-time tokens are issued per purpose so a reset token cannot verify an email.
Fix. Use one of the purposes the message lists.
E205 — KARNI_ROLES is malformed¶
exception · from karni.accounts.roles.declared_roles
Why Karni objects. The role declaration is the single source of what each role may do; a malformed entry cannot be reconciled into the role tables.
Fix. Write KARNI_ROLES = {'
E206 — A grant in KARNI_ROLES cannot be resolved¶
exception · from karni.accounts.roles.sync_roles
Why Karni objects. A pattern that matches no permission, an 'own' scope on a model with no owner field, a 'related' scope on a model without that scope path, or a 'custom' scope nobody registered would grant nothing silently; Karni refuses instead.
Fix. Fix the key (permissions look like 'events.event:view'; extra actions come from class Karni: actions), declare owner_field / scope_paths in the model's class Karni, or register the custom scope with karni.accounts.roles.register_scope.
E207 — user.can() was given an action without a model¶
exception · from karni.accounts.roles.user_can
Why Karni objects. A permission is a (model, action) pair; a bare action name cannot be looked up.
Fix. Pass the full key, user.can('shop.order:approve'), or name the model: user.can('approve', obj=order) or user.can('approve', model=Order).
E208 — The request lacks the permission @requires names¶
API 400 response · from karni.accounts.roles.requires
Why Karni objects. A view or service guarded with @requires('app.model:action') was called by a user whose roles hold no grant for it (a 403).
Fix. Grant the permission to one of the user's roles in KARNI_ROLES and run karni sync-roles, or assign the user a role that has it.
E209 — Can() was given a bad permission, or one for another model¶
exception · from karni.api.access.Can
Why Karni objects. Can() names permissions as 'app_label.model:action', and its row filter is built against that model, so a viewset's declaration can only use its own model's permissions.
Fix. Write Can('events.event:approve'); on EventViewSet it must be an events.event permission. To gate on another model's permission use HasRole(...).
E210 — scope_exempt = True without a reason¶
exception · from karni.api.access.validate_access
Why Karni objects. A viewset or admin that shows every row its gate admits is the one place row scoping is off; without a reason it looks like an oversight, and karni check lists every exemption by its reason.
Fix. Add scope_exempt_reason = '...' next to scope_exempt = True (e.g. 'public catalogue'), or remove the exemption and let Granted() / the roles' grants scope the rows.
E211 — Granted() has no permission for this action or viewset¶
exception · from karni.api.access.Granted
Why Karni objects. Granted() resolves to the viewset's model and the current action; an unsafe custom @action the model never declared has no permission to check, and a viewset without a model has nothing to resolve against. Evaluating it outside a viewset is the same error.
Fix. Add the action to class Karni: actions = [...] on the model and grant 'app.model:model = ... on the viewset, or evaluate through action_predicate(view, access).
E212 — Karni.field_roles is malformed, names a missing field or an undeclared role¶
system check (error) · from karni.meta.field_roles; karni.checks.check_field_roles
Why Karni objects. Field-level visibility is declared once on the model as field -> roles; a typo in a field name hides nothing, and a role nobody holds hides the field from everyone, so both are refused at boot.
Fix. Write field_roles = {'phone': {'read': ['staff', 'support'], 'write': ['staff']}} (or 'phone': ['staff'] for read and write) with field names of the model and role slugs declared in KARNI_ROLES.
E213 — The permission matrix refused a change¶
API 400 response · from karni.accounts.matrix.apply_changes
Why Karni objects. A guard rail stopped the request and nothing was written: a system role's declared grant is locked, the operator does not hold the permission (or holds it at a narrower scope than the one granted), the change would remove the operator's own last permission to change permissions, or the role, permission or scope is unknown.
Fix. Change KARNI_ROLES for a system role's grants; ask someone who holds the permission at that scope; keep one of your roles able to change permissions; check the role slug, the 'app.model:action' key and the scope value against GET permissions/matrix/.
E214 — A role's scope names a path the model does not have¶
system check (error) · from karni.checks.check_role_scope_paths
Why Karni objects. A grant's scope becomes a database filter on every request: own follows Karni.owner_field, related:<key> follows Karni.scope_paths[key], and a custom:<name> scope builds its own. A path the model does not have would answer 500 to every user holding the grant — typically after a new model joins a wildcard grant written for its siblings.
Fix. Give the model the path the scope expects (class Karni: owner_field = 'workspace__members', or the scope path), teach the custom scope the new model, or narrow the grant's pattern so it does not cover the model.
3xx · Admin¶
E301 — KARNI_ADMIN_BASE is not a ModelAdmin class¶
exception · from karni.admin.base
Why Karni objects. Every auto-registered admin is built on this base class.
Fix. Point KARNI_ADMIN_BASE at an importable django.contrib.admin.ModelAdmin subclass (default 'unfold.admin.ModelAdmin').
E302 — AdminMeta.inline_on is invalid¶
exception · from karni.admin.inlines
Why Karni objects. inline_on names the foreign key under whose parent the model is shown inline.
Fix. Name a ForeignKey field of the model: class AdminMeta: inline_on = 'event'.
E303 — KARNI_ADMIN_LOGO is not a logo declaration¶
exception · from karni.conf.admin_logo.unfold_logo_entries
Why Karni objects. The admin logo is wired into UNFOLD from this one setting, so it must name static files unfold can serve.
Fix. Set KARNI_ADMIN_LOGO = 'karni' for Karni's mark, or {'light': '
4xx · Events, listeners, schedules and Celery¶
E401 — A queued listener is not a module-level function¶
exception · from karni.events.queued
Why Karni objects. A queued listener runs on a Celery worker, which finds it by its dotted name; nested functions and lambdas have none.
Fix. Define the listener at the top level of
E402 — A listener registration is invalid¶
exception · from karni.events
Why Karni objects. Listeners connect to model events (on_created(Model), ...) and must be callables.
Fix. Decorate a function, and pass a model class or an 'app_label.ModelName' string: @on_created(Event) or @on_created('events.Event').
E403 — A Celery task cannot be found¶
exception · from karni.celery.resolve_task
Why Karni objects. Tasks are found by dotted name; Celery autodiscovers only @shared_task functions in installed apps' tasks.py.
Fix. Check the dotted name, and that the function is decorated with @shared_task in an installed app's tasks.py.
E404 — A schedule declaration is invalid¶
exception · from karni.schedule (every, cron)
Why Karni objects. Schedules are stored by django-celery-beat: the task must be a task or its dotted name, and name/args/kwargs/options must be plain JSON-serializable values.
Fix. Pass a @shared_task or 'app.tasks.task_name', a non-empty name, args as a list, kwargs and options as dicts of plain values (ids, not objects).
E405 — A schedule's interval or cron expression is invalid¶
exception · from karni.schedule (every, cron)
Why Karni objects. Intervals are whole positive seconds; cron expressions have five fields.
Fix. Use every('15m') / every(minutes=15) (s, m, h, d units), or cron('0 3 * * *') ('minute hour day-of-month month day-of-week').
E406 — Two schedules share a name¶
system check (error) · from karni.schedule; karni.checks.check_schedules
Why Karni objects. Schedule names become django-celery-beat PeriodicTask names, so they must be unique across the project.
Fix. Pass a distinct name=... to one of the two declarations the message names.
E407 — No schedule has that name¶
exception · from karni.schedule.run_now; karni schedule run
Why Karni objects. Schedules are looked up by name.
Fix. List them with karni schedule list and use one of the names shown.
E408 — A scheduled task failed when run by hand¶
exception · from karni_schedules management command; karni schedule run
Why Karni objects. karni schedule run executes the task in-process so failures show immediately.
Fix. Read the task's own error in the message and fix the task; re-run with karni schedule run <name>.
5xx · Settings and optional dependencies¶
E501 — An optional dependency is not installed¶
exception · from karni.conf.sentry, karni.storage, karni.accounts.twofa, karni.api.search.postgres
Why Karni objects. Optional features (Sentry, S3 storage, thumbnails, 2FA, PostgreSQL search) need extra packages that a minimal install leaves out.
Fix. Install the extra the message names, e.g. pip install 'karni[sentry]', 'karni[s3]', 'karni[images]', 'karni[2fa]' or 'psycopg[binary]'.
W501 — The throttle cache is private to each process¶
system check (warning) · from karni.checks.check_throttle_cache
Why Karni objects. DRF keeps rate-limit counters in CACHES['default']. With DEBUG off, a locmem or dummy backend gives every worker its own counters, so the login, registration and password-reset limits are multiplied by the worker count.
Fix. Point CACHES['default'] at a backend every worker shares (Redis — Karni's default — or the database cache), or silence karni.W501 for a single-process deployment.
E502 — Unknown settings profile¶
exception · from karni.conf.karni_settings
Why Karni objects. karni_settings() picks dev or prod defaults from DJANGO_ENV.
Fix. Set DJANGO_ENV to one of the profiles the message lists, or pass env_name= to karni_settings().
E503 — A module's settings fragment is invalid¶
exception · from karni.conf (karni.settings entry points)
Why Karni objects. Installed modules contribute default settings; a fragment may only add new top-level names, so installing a module never changes a setting you rely on.
Fix. Fix or upgrade the module the message names. Module authors: export a dict of new names only.
E504 — Karni settings failed validation¶
exception · from karni.conf.validation.validate_settings (at startup)
Why Karni objects. Settings are validated once at boot so a bad value fails the deploy, not the first request that reads it.
Fix. Fix every setting the message lists. KARNI_VALIDATE = False skips validation (not recommended).
E505 — The welcome page is reachable with DEBUG off¶
system check (error) · from karni.checks.check_welcome_route
Why Karni objects. The welcome page (karni.welcome) introspects the project at request time — models, modules, status checks, the agent context — for first boot in development; served in production it would describe the deployment to anyone who opens the site.
Fix. Mount it only through *karni.welcome.welcome_urls() at the end of urlpatterns (it returns no route when DEBUG is False) and remove any hand-written path('', WelcomeView.as_view()).
E506 — A setting names a dotted path that cannot be imported¶
exception · from karni.imports.import_setting (KARNI_SEARCH_PROVIDER, KARNI_API_ROUTER, KARNI_ADMIN_SITE, KARNI_THUMBNAILER, ...)
Why Karni objects. Settings that name a class or function by dotted path are resolved when first used; a typo or a package missing from the environment would otherwise surface as a bare ModuleNotFoundError far from the setting.
Fix. Fix the path the message names ('package.module.Object') or install the package that provides it.
E507 — KARNI_CACHE names an unknown cache backend¶
exception · from karni.conf (KARNI_CACHE)
Why Karni objects. The default cache (throttle counters, response cache, locks) is Redis at REDIS_URL; locmem is the in-process alternative for a host run without Redis.
Fix. Set KARNI_CACHE to redis (the default) or locmem; for anything else define CACHES yourself in settings.py.
6xx · Testing and seeding¶
E601 — The resource under test is not routed¶
test failure · from karni.testing.assert_crud / assert_isolated
Why Karni objects. assert_crud tests a resource through its URLs, exactly as clients reach it.
Fix. Route the viewset (karni make viewset
E602 — The API does not behave as its access declaration promises¶
test failure · from karni.testing.assert_crud
Why Karni objects. assert_crud derives every expected outcome (allowed actions, 403/404 denials, pagination, filtering, owner stamping, response schema) from the viewset's access and the OpenAPI schema; each listed failure is a real difference.
Fix. Fix each failure in the report: usually the access declaration, a custom get_queryset, or a serializer that does not match the schema. skip=[...] excludes a check that genuinely does not apply.
E603 — assert_crud(skip=...) names unknown checks¶
exception · from karni.testing.assert_crud
Why Karni objects. Skipping a check that does not exist would hide nothing and mislead the reader.
Fix. Use the check names the message lists.
E604 — A request raised an exception during assert_crud¶
test failure · from karni.testing.assert_crud
Why Karni objects. The API raised instead of answering, so the check could not run.
Fix. Read the exception in the message; fix the view, serializer or data it names.
E605 — assert_isolated cannot isolate this resource¶
exception · from karni.testing.assert_isolated; karni.testing.isolation
Why Karni objects. Isolation is proved along a dimension (owner, tenant, ...) that the resource's access actually filters by.
Fix. Give the resource an Owned(...) access (or install the module providing the dimension), and name only registered dimensions.
E606 — Rows leak to a user who must not see them¶
test failure · from karni.testing.assert_isolated
Why Karni objects. One owner (or tenant) could read or change another's rows: a data leak.
Fix. Restrict the viewset's access with Owned('
E607 — There is no OpenAPI schema snapshot yet¶
test failure · from karni.testing.assert_schema_snapshot
Why Karni objects. CI compares the API schema with a committed snapshot so an accidental breaking change fails the build; CI never writes snapshots.
Fix. Generate it locally with the command in the message and commit the file.
E608 — The OpenAPI schema changed¶
test failure · from karni.testing.assert_schema_snapshot
Why Karni objects. Clients (and the generated frontend types) depend on the schema; this change would reach them.
Fix. If the change is intended, accept it with the command in the message and commit the snapshot; otherwise revert the change that caused it.
E609 — No factory can be built for this model¶
exception · from karni.testing.factories
Why Karni objects. Factories create rows of concrete models.
Fix. Pass a concrete model class or an 'app_label.Model' string.
E610 — A factory cannot generate a value for a field¶
exception · from karni.testing.factories
Why Karni objects. Values are inferred from field types and names; some custom fields have no safe default.
Fix. Pass the value (create(Model, field=...)) or define an explicit factory in
E611 — Required relations form a loop¶
exception · from karni.testing.factories; karni.seeding
Why Karni objects. Each row needs its required relation to exist first; in a loop, no order works.
Fix. Make one of the foreign keys nullable, pass an existing row for that field, or seed one model yourself in a Seeder.seed() override.
E612 — Seeding keeps hitting a database constraint¶
exception · from karni.seeding
Why Karni objects. Generated values collided with a unique or check constraint on every attempt.
Fix. Give the clashing fields distinct values in a seeder (karni make seeder
E613 — Seeding a row failed¶
exception · from karni.seeding
Why Karni objects. The model rejected a generated row (model validation or save() logic).
Fix. Override Seeder.seed() for the model (karni make seeder
E614 — karni seed was given invalid arguments¶
exception · from karni seed; manage.py karni_seed
Why Karni objects. --model takes app_label.Model names and --count a non-negative number.
Fix. Fix the argument the message names, e.g. karni seed --model events.Event --count 20.
E615 — karni seed refuses to run with DEBUG = False¶
exception · from karni seed; manage.py karni_seed
Why Karni objects. DEBUG = False usually means a production database, and seeding creates a demo superuser with a known password.
Fix. Seed a development database. If you really mean it, pass --force.
7xx · Agent tooling: karni check, karni.rules.py, changelog¶
E701 — karni.rules.py is invalid¶
exception · from karni.agent.overrides
Why Karni objects. karni.rules.py adjusts the agent rules; it is read statically (never executed), so only literal values are allowed.
Fix. Fix the line the message names: literal ADD/OVERRIDE/DISABLE/ADAPTERS values; ADD codes like P001 with a non-empty 'rule'; OVERRIDE/DISABLE only Karni K codes.
E702 — karni check found violations¶
exception · from karni check; manage.py karni_check
Why Karni objects. The architecture checks enforce the K rules (generated code is not hand-written, access is declared, and so on).
Fix. Fix each listed violation (each names its K code; karni explain K106 explains one), or suppress a deliberate exception the way the violation describes.
E703 — A source file cannot be parsed¶
exception · from karni.agent.check
Why Karni objects. karni check reads your code statically; a syntax error stops it.
Fix. Fix the syntax error at the file and line in the message.
E704 — Karni's changelog is invalid¶
exception · from karni.changelog
Why Karni objects. karni upgrade reads the changelog to find and apply every breaking change.
Fix. Maintainers: fix the entries the message lists (schema in karni/changelog/init.py). Users: reinstall karni-cli.
E801 — An import cannot proceed as asked¶
API 400 response · from karni.transfer
Why Karni objects. The mapping names a column the file lacks or a field the role may not write, the file has no header row, a commit was requested before the mandatory preview, or the model's import metadata (Karni.natural_key / import_key / import_lookup / import_transform / after_import) is malformed.
Fix. Map columns to importable fields only (GET /
E802 — The file format is not supported¶
API 400 response · from karni.transfer.formats
Why Karni objects. Import and export speak CSV and XLSX; anything else (or an upload whose format cannot be told from its name or content type) is refused.
Fix. Upload a .csv or .xlsx file, or pass ?format=csv|xlsx; XLSX needs openpyxl (a core dependency).
E803 — Too many rows for one import or export¶
API 400 response · from karni.transfer
Why Karni objects. KARNI_IMPORT_MAX_ROWS caps an upload and KARNI_EXPORT_MAX_ROWS an export so one request cannot pin a worker or build an unbounded file.
Fix. Split the upload, narrow the export's filters, or raise the setting for a project that needs more.
APR (APR)¶
APR001 — The change was sent for approval instead of being written¶
API 400 response · from karni_approvals.interception
Why Karni objects. The model declares requires_approval and the change touches a guarded field, so a pending change holds it until a second person approves (the API answers 202 with the pending change; the admin says so; code gets KarniWriteDeferred).
Fix. Nothing is wrong. Have a checker approve it (the admin queue, /api/pending-changes/,
APR002 — The maker cannot be the checker¶
API 400 response · from karni_approvals.services.decide
Why Karni objects. Second-person approval means a second person: the user who proposed a change may never approve or reject it, through the API, the admin or a direct call.
Fix. Ask another holder of the approve permission to decide it.
APR003 — This user may not decide this pending change¶
API 400 response · from karni_approvals.services.decide
Why Karni objects. Deciding needs the karni_approvals.pendingchange:approve (or :reject) grant, the model's approver_roles when it declares them, and the row inside the checker's own scope.
Fix. Grant the permission in KARNI_ROLES (or the permission matrix), assign the approver role, or have someone whose scope covers the row decide.
APR004 — This checker has already decided this change¶
API 400 response · from karni_approvals.services.decide
Why Karni objects. One decision per person per change; a second one would count twice.
Fix. Another checker must supply the remaining approvals.
APR005 — The pending change is no longer pending¶
API 400 response · from karni_approvals.services.decide
Why Karni objects. It was already approved, rejected, expired or failed; decisions are final.
Fix. Propose the change again if it is still wanted.
APR006 — The change could not be applied: the row changed since it was proposed¶
API 400 response · from karni_approvals.services.apply
Why Karni objects. Every recorded old value must still be current when the change is applied, or an approval would overwrite something nobody reviewed.
Fix. Propose the change again against the row as it is now.
APR007 — The model's approval policy is malformed¶
system check (error) · from karni_approvals.meta.approval_policy
Why Karni objects. requires_approval, approvals_required, approver_roles, approval_timeout and approval_on_timeout are read at boot; a typo would silently hold or release changes.
Fix. requires_approval = True or a list of field names; approvals_required a positive int; approver_roles / approval_escalation_roles lists of KARNI_ROLES slugs; approval_timeout a timedelta or hours; approval_on_timeout 'expire' or 'escalate'.
IMP (IMP)¶
IMP001 — Impersonation needs the explicit impersonate permission¶
API 400 response · from karni_impersonation.services.start
Why Karni objects. Acting as someone else is never implied by staff or admin status; only a role granted karni_impersonation.impersonationsession:impersonate may start a session.
Fix. Grant the permission to the support role in KARNI_ROLES (or the permission matrix).
IMP002 — The target holds something the impersonator does not¶
API 400 response · from karni_impersonation.services.privilege_gap
Why Karni objects. Impersonation must never escalate privileges: a target who is a superuser, is staff, or holds any grant or Django permission the impersonator lacks cannot be impersonated.
Fix. Impersonate from an account that holds at least everything the target holds, or ask such a person to.
IMP003 — A reason is required to impersonate¶
API 400 response · from karni_impersonation.services.start
Why Karni objects. KARNI_IMPERSONATION_REQUIRE_REASON is on (the default for customer data): the reason is recorded with the session and sent to the impersonated user.
Fix. Give a reason, or set KARNI_IMPERSONATION_REQUIRE_REASON = False for a project that does not need one.
IMP004 — This user cannot be impersonated now¶
API 400 response · from karni_impersonation.services.start
Why Karni objects. You cannot impersonate yourself or an inactive account, or start a second session while one is running.
Fix. Stop the current session first, or pick another user.
IMP005 — This request is not part of an impersonated session¶
API 400 response · from karni_impersonation.viewsets.ImpersonationSessionViewSet.stop
Why Karni objects. stop/ ends the session the request belongs to; there is none (it ended, expired or never started).
Fix. Nothing to do: you are yourself.
IMP006 — This action is blocked while impersonating¶
API 400 response · from karni_impersonation.vetoes, karni_impersonation.middleware
Why Karni objects. KARNI_IMPERSONATION_BLOCKED names what an impersonator may never do (deleting, exporting, payment changes, credential changes by default), whatever the target may.
Fix. Stop impersonating and do it as yourself, or ask the user to; a project narrows the list in KARNI_IMPERSONATION_BLOCKED.
IMP007 — karni-impersonation is installed but not wired¶
system check (error) · from karni_impersonation.checks
Why Karni objects. The middleware resolves the impersonated session, enforces the limit and injects the banner; the URL include serves the banner's exit. Without them sessions start but nothing acts on them.
Fix. Add 'karni_impersonation.middleware.ImpersonationMiddleware' after AuthenticationMiddleware in MIDDLEWARE and path('impersonation/', include('karni_impersonation.urls')) to config/urls.py.
IMP008 — KARNI_IMPERSONATION_BLOCKED is malformed¶
system check (error) · from karni_impersonation.checks
Why Karni objects. The blocked list is matched on every gate; a non-string entry would silently match nothing.
Fix. Use strings: action names, 'app.model', 'app.model:action' patterns, or '/path/*'.
karni-payments (PAY)¶
PAY001 — KARNI_PAYMENTS_PROVIDER cannot be loaded¶
exception · from karni_payments.providers.base.get_provider
Why Karni objects. Every checkout, refund and webhook goes through the configured provider class.
Fix. Set KARNI_PAYMENTS_PROVIDER to a shortcut ('stripe', 'razorpay', 'test') or the dotted path of a karni_payments.providers.base.PaymentProvider subclass.
PAY002 — No payment provider has that name¶
exception · from karni_payments.providers.base.get_provider_by_name
Why Karni objects. Webhooks and stored payments name the provider that handled them.
Fix. Use the configured provider's name or a shortcut: stripe, razorpay, test.
PAY003 — The payment provider's SDK is not installed¶
exception · from karni_payments.providers (stripe, razorpay)
Why Karni objects. Provider SDKs are optional extras so a project installs only the one it uses.
Fix. Install the extra: pip install 'karni-payments[stripe]' or 'karni-payments[razorpay]'.
PAY004 — The payment provider's API credentials are not set¶
exception · from karni_payments.providers (stripe, razorpay)
Why Karni objects. The provider cannot create checkouts or refunds without its API keys.
Fix. Set STRIPE_SECRET_KEY (Stripe) or RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET (Razorpay) in the environment (.env in development). Use the 'test' provider for local work without keys.
PAY005 — The webhook signing secret is not set¶
exception · from karni_payments.providers (stripe, razorpay)
Why Karni objects. Unverified webhooks could mark any payment as paid, so Karni refuses them.
Fix. Set STRIPE_WEBHOOK_SECRET or RAZORPAY_WEBHOOK_SECRET to the secret shown in the provider's dashboard for this webhook endpoint.
PAY006 — A webhook failed signature verification¶
exception · from karni_payments.webhooks (answered with HTTP 400, never stored)
Why Karni objects. The payload was not signed with this endpoint's secret: a forged request, or a secret that does not match the provider's dashboard.
Fix. If the webhook is genuine, copy the endpoint's signing secret from the provider's dashboard into the *_WEBHOOK_SECRET setting. Otherwise nothing to do: the request was rejected.
PAY007 — The currency or amount cannot be charged exactly¶
exception · from karni_payments.providers.base.to_minor_units
Why Karni objects. Providers charge integer minor units (cents); Karni never rounds money silently, and 3-decimal currencies are not supported yet.
Fix. Round the amount to the currency's minor unit before charging (e.g. Decimal('10.50')), and charge in a 0- or 2-decimal currency.
PAY008 — A checkout redirect URL points at an origin that is not allowed¶
API 400 response · from karni_payments.services.create_checkout
Why Karni objects. An open redirect after payment would let attackers send customers anywhere.
Fix. Redirect to your frontend (KARNI_FRONTEND_URL), or add the origin to KARNI_PAYMENTS_ALLOWED_REDIRECTS.
PAY009 — A webhook event matches no local payment¶
exception · from karni_payments.services (webhook processing task)
Why Karni objects. Providers can deliver a webhook before the checkout that created the payment is committed; the task retries instead of dropping the event.
Fix. Usually nothing: the retry succeeds. If it keeps failing, the payment was created outside Karni (or in another environment sharing the webhook): check the provider ids in the message.
The karni command (C)¶
C001 — Not inside a Karni project¶
karni command · from karni_cli.project.require_project and every project command
Why Karni objects. Project commands find the project by walking up to the directory with manage.py (and config/settings.py).
Fix. cd into the project (or a directory below it), or create one: karni new
C002 — No way to run the project's Python¶
karni command · from karni_cli.project (runners: serve, migrate, test, shell, seed, ...)
Why Karni objects. Project commands run in the project's own interpreter: its .venv, else the active virtualenv (VIRTUAL_ENV), else the running docker compose stack.
Fix. Create the venv (python -m venv .venv && .venv/bin/pip install -e .), activate one that has the project's dependencies, or start the stack (docker compose up -d). The message says which one the command looked for.
C003 — Docker is not installed¶
karni command · from karni_cli.project
Why Karni objects. Without --local, karni runs the project through docker compose.
Fix. Install Docker with the compose plugin, or run on the host: karni serve --local (with a .venv).
C004 — These options cannot be combined¶
karni command · from karni serve, karni make model/page/schedule
Why Karni objects. Some options only apply in a particular mode (for example --host needs --local).
Fix. Drop one of the options as the message says, or add the option it needs.
C005 — This karni-cli install is incomplete¶
karni command · from karni_cli (bundled templates, agent rules, changelog, error codes)
Why Karni objects. The wheel bundles the project template, agent rules, changelog and error codes; one is missing, so the install is broken.
Fix. Reinstall karni-cli (pipx reinstall karni-cli, or pip install --force-reinstall karni-cli).
C006 — An optional tool is not installed¶
karni command · from karni mcp, karni types
Why Karni objects. Some commands need extra tools: the MCP SDK for karni mcp, Node and openapi-typescript for karni types.
Fix. Install what the message names, e.g. pip install 'karni-cli[mcp]', or add openapi-typescript to frontend devDependencies (Node 20+).
C007 — A configuration value is invalid¶
karni command · from karni config, KARNI_AGENT_SOURCE, KARNI_TEMPLATES, KARNI_UI_SRC
Why Karni objects. karni reads its settings from karni config files and a few environment variables.
Fix. Use a value the message lists (karni config shows the known settings), or unset the environment variable.
C008 — A file cannot be written¶
karni command · from karni config
Why Karni objects. The file or its directory is not writable.
Fix. Check the path's permissions, or set the value per project instead of --global.
C009 — A command in the project failed¶
karni command · from karni context, karni upgrade, karni rules (project interpreter)
Why Karni objects. karni ran manage.py or Python in the project and it exited with an error.
Fix. Read the project's error in the message (often a settings or import error), fix it, and re-run. karni doctor checks the environment.
C010 — The path is not a file inside the project¶
karni command · from karni rules --check, karni context --with-history
Why Karni objects. These commands read files of the current project only.
Fix. Pass an existing file path inside the project.
C011 — pytest is not installed in the project interpreter¶
karni command · from karni test
Why Karni objects. karni test runs python -m pytest through the project's interpreter (its .venv, the active virtualenv, or the compose service); pytest and pytest-django are in the project's dev dependency group, not in karni.
Fix. Install them there: pip install pytest pytest-django (or uv sync for the dev group), then run karni test again.
C101 — The file karni make would write already exists¶
karni command · from karni make (every generator)
Why Karni objects. Generators never overwrite your code silently.
Fix. Pass --force to overwrite (or, for multi-file generators, to create only the missing files), or remove the file first.
C102 — Which app? (none, unknown, or several)¶
karni command · from karni make (generators that write into an app)
Why Karni objects. Generators write into a local app listed in the # karni:apps block of config/settings.py.
Fix. Create one (karni make app
C103 — The name is not valid here¶
karni command · from karni make, karni new
Why Karni objects. Names become Python modules, classes, URLs or task names, each with its own rules, and must not shadow existing modules or Django/Karni commands.
Fix. Use the form the message shows (snake_case module, CamelCase model, kebab-case module or route) and a name nothing else uses.
C104 — A field spec is invalid¶
karni command · from karni make model (name:type[:arg][?] fields)
Why Karni objects. Fields are written as name:type[:arg][?], e.g. title:str:200 or starts_at:datetime?; relations name their target model.
Fix. Fix the field the message names; karni make model --help lists the types.
C105 — Something the generator builds on does not exist yet¶
karni command · from karni make (viewset, test, serializer, listener, schedule, ...)
Why Karni objects. Generators wire new code into existing code: a viewset needs its model, a test its viewset, a listener its event.
Fix. Run the karni make command the message names first, then re-run this one.
C106 — An option value is invalid¶
karni command · from karni make, karni types
Why Karni objects. The option accepts specific values (the message lists them).
Fix. Use one of the values shown, e.g. --access owned:organizer or --every 15m.
C107 — The new code would clash with existing code¶
karni command · from karni make (component, viewset, schedule, page, service)
Why Karni objects. Two components claiming one role, two viewsets for one model, or two schedules or routes with one name would shadow each other.
Fix. Extend or edit the existing code the message names, or choose another name.
C108 — A karni marker is missing from a project file¶
karni command · from karni make, karni add (settings.py and routes.tsx markers)
Why Karni objects. Generators insert code at # karni: / // karni: markers that karni new writes; older or hand-edited projects may lack them.
Fix. Add the change by hand as the message shows, or restore the marker from a fresh karni new project.
C109 — An explicit serializer needs a reason¶
karni command · from karni make serializer
Why Karni objects. BaseViewSet generates the serializer from the model (rule K106); a hand-written one must say why it exists.
Fix. Pass --reason "
C110 — A project file cannot be parsed¶
karni command · from karni make (reads your Python before editing it)
Why Karni objects. karni make edits your files structurally and stops rather than guess.
Fix. Fix the syntax error at the file and line in the message, then re-run.
C199 — Internal error in karni¶
karni command · from karni make, karni upgrade
Why Karni objects. A generator or codemod hit a case it does not handle; nothing was written.
Fix. Report it with the command and the file it stopped on. Meanwhile, make the change by hand.
C201 — karni upgrade needs a clean git work tree¶
karni command · from karni upgrade
Why Karni objects. Codemods rewrite many files; a clean tree lets you review and undo them with git.
Fix. Commit or stash first, preview with --dry-run, or pass --allow-dirty to write anyway.
C202 — karni upgrade does not downgrade¶
karni command · from karni upgrade
Why Karni objects. Codemods only move code forward.
Fix. Pass a --to version newer than the project's karni, or check --from.
C203 — That is not a version number¶
karni command · from karni upgrade (--to / --from)
Why Karni objects. Versions are MAJOR.MINOR[.PATCH].
Fix. Write e.g. --to 0.2 or --to 0.2.0.
C204 — No such rule or procedure¶
karni command · from karni rules, karni explain
Why Karni objects. Rules (K codes) and procedures are listed by karni rules.
Fix. Run karni rules for the codes and procedures, then use one of them.
C205 — The agent adapter configuration is invalid¶
karni command · from karni rules sync (karni.rules.py ADAPTERS)
Why Karni objects. Adapters are the generated agent guidance files; each has an id and a line cap.
Fix. Use the adapter ids the message lists, and shorten project rules feeding an adapter that exceeds its cap.
C206 — The OpenAPI schema cannot be turned into types¶
karni command · from karni types
Why Karni objects. karni types reads the backend's schema (x-karni-resource operations) to generate typed hooks.
Fix. Make sure the backend serves Karni viewsets (karni routes) and the schema uses only local $refs.
C207 — karni add cannot edit pyproject.toml¶
karni command · from karni add
Why Karni objects. karni add manages the [project].dependencies table of the project's pyproject.toml.
Fix. Add the table (or the dependency) by hand, then re-run.
Agent rules (K)¶
karni rules lists them in one screen; karni check and the architecture tests (pytest --pyargs karni.testing.architecture) enforce them.
K001 — karni context first; trust it.¶
agent rule, must · enforced by review
Rule. Run karni context before reading any files. It prints the current apps, models, viewsets, UI components, hooks, pages and installed modules in a few hundred tokens. Trust it.
Why Karni objects. The inventory is derived live from the app registry, the router and the @karni/ui manifest, so it is never stale and costs far fewer tokens than crawling source.
Fix.
K002 — Rules beat other frameworks' habits.¶
agent rule, must · enforced by review
Rule. Run karni rules at the start of a session and follow what it returns; the adapter files (AGENTS.md, CLAUDE.md, ...) only point here.
Why Karni objects. One source of truth exposed through the CLI cannot drift between tools.
Fix.
K003 — Model changed: make migrate; karni types.¶
agent rule, must · enforced by review
Rule. After any model change run make migrate && karni types.
Why Karni objects. Migrations and the frontend's TS types + typed hooks are derived from the models; skipping either leaves the API and the UI out of sync.
Fix.
K004 — karni rules --check FILE; karni check pre-commit.¶
agent rule, must · enforced by review
Rule. Run karni rules --check <file> on files you write and karni check before committing — every enforced rule fails the build, nothing only warns.
Why Karni objects. Enforced rules fail CI anyway; catching them while editing is cheaper.
Fix.
K005 — Opt-outs need an inline reason.¶
agent rule, error · enforced by opt-out-reason, eslint-comments/require-description
Rule. Opt-outs are inline and REQUIRE a reason: Python # karni: allow(<rule-id>): <why>; ESLint // eslint-disable-next-line karni/<rule> -- <why>; jscpd // jscpd:ignore-start -- <why>.
Why Karni objects. Rules without escape hatches cause forks, and a fork is worse than a duplicate — but every exception must say why so it can be reviewed.
Fix. Write the reason after the marker.
K006 — Procedures: karni rules --procedure NAME.¶
agent rule, should · enforced by review
Rule. Prefer the standard procedures for routine work: new-model, new-page, add-module, new-module — karni rules --procedure <name>, or the matching slash command where your tool has one.
Why Karni objects. The procedures encode the shortest correct path for routine work.
Fix.
K007 — Commands: karni --help.¶
agent rule, should · enforced by review
Rule. Use the project commands (karni --help lists them all): make up starts the stack; make migrate runs makemigrations + migrate; karni types regenerates TS types + typed hooks from the API schema; karni check runs architecture and system checks; karni context prints the inventory; karni make <type> scaffolds; runners like karni test, karni shell, karni routes, karni worker, karni schedule list wrap manage.py/celery/pytest.
Why Karni objects. They run inside the right interpreter (project .venv or compose service).
Fix.
K009 — Scaffold with karni make.¶
agent rule, must · enforced by review
Rule. Create apps, models, viewsets, serializers, migrations, tasks, schedules, events, listeners, management commands, services, middleware, pages, components and modules with karni make <type> (karni make --list; --dry-run previews, --help per type). Never hand-create a file, class or route a generator makes — run it, then edit what it wrote.
Why Karni objects. Generators emit the conventions in one step (BaseModel, a mandatory access declaration, settings and route markers, ruff-clean imports) and are tested to pass every rule; hand-written scaffolding drifts.
Fix. Delete the hand-written scaffold and run the matching karni make command.
K101 — Models inherit BaseModel or compose its mixins.¶
agent rule, error · enforced by model-base
Rule. Models inherit karni.models.BaseModel (= UUIDModel + TimestampedModel + SoftDeleteModel + AuditedModel), or compose those mixins directly when a model should not have all four (e.g. an append-only log: UUIDModel, TimestampedModel). Declare APIMeta / AdminMeta only for fields that must be hidden, read-only or write-only.
Why Karni objects. The mixins supply uuid ids, timestamps, soft delete, audit stamping and the managers the derived API/admin rely on.
Fix. Inherit karni.models.BaseModel (or compose its mixins).
K102 — Viewsets inherit BaseViewSet.¶
agent rule, error · enforced by viewset-base
Rule. Viewsets inherit karni.api.BaseViewSet — usually just model = X plus its access declaration (K109) (+ search_fields); the auto-router registers it.
Why Karni objects. BaseViewSet derives serializer, filters, pagination, permissions and schema.
Fix. Inherit karni.api.BaseViewSet and set model = X.
K103 — No ModelAdmin; use AdminMeta.¶
agent rule, error · enforced by admin-convention
Rule. Do not register a plain ModelAdmin. Declare AdminMeta on the model (the admin is derived), or subclass karni.admin.KarniModelAdmin.
Why Karni objects. The derived admin stays consistent with the model and the API.
Fix. Delete the admin class and use AdminMeta, or subclass KarniModelAdmin.
K104 — Raw SQL only under /sql/.¶
agent rule, error · enforced by raw-sql
Rule. No raw SQL (.raw(), cursor.execute(), RawSQL) outside */sql/* or migrations.
Why Karni objects. Raw SQL bypasses managers (soft delete, owner scoping) and the ORM's safety.
Fix. Use the ORM, or move the query into a sql/ module.
K105 — HTTP clients only in /services/.¶
agent rule, error · enforced by http-client
Rule. Import requests / httpx / urllib.request only in */services/* or */providers/* modules.
Why Karni objects. External calls in one layer are mockable, retryable and auditable.
Fix. Move the call into a services module and call that.
K106 — No Meta-only ModelSerializer.¶
agent rule, error · enforced by redundant-serializer
Rule. Never write a Meta-only ModelSerializer; BaseViewSet(model=X) generates it.
Why Karni objects. A redundant serializer silently diverges from the generated one.
Fix. Delete it; customise via APIMeta or override get_serializer_class().
K107 — No duplicated code.¶
agent rule, error · enforced by duplicate
Rule. No structurally duplicated code (8+ identical statements in two places); reuse the named Karni API or extract one implementation.
Why Karni objects. Duplicates are where boilerplate creeps back in.
Fix. Call the existing implementation instead of copying it.
K108 — Off-request writes: current_user(u).¶
agent rule, must · enforced by review
Rule. AuditedModel stamps created_by/updated_by from the current request. Outside a request (Celery tasks, management commands) nothing is stamped unless you wrap the writes in with current_user(user): from karni.middleware. Never assign created_by/updated_by by hand.
Why Karni objects. One stamping path keeps the audit trail trustworthy under WSGI, ASGI and workers.
Fix. from karni.middleware import current_user; with current_user(user): ...
K109 — Viewsets declare access.¶
agent rule, error · enforced by viewset-access
Rule. Every BaseViewSet declares access: Granted() (the roles' grants for the model, row-scoped; the default) or Access(read=..., write=...) built from Owned("<fk to user>"), HasRole(...), Authenticated(), Public() and Where(lambda request: Q(...)) with | / &. Reads are public only when you write Public(). No access denies every request; Unscoped(reason=...) is the explicit opt-out.
Why Karni objects. One declaration compiles into both the request gate and the row filter, so neither can be forgotten; a forgotten permission class fails open silently.
Fix. Add access = Granted() (or Access(read=..., write=...) for a custom rule).
K110 — Row rules: in access.¶
agent rule, error · enforced by access-override
Rule. Express who sees which rows in access (Where(...) for custom logic, @action(..., access=...) for one action). An overridden get_queryset is still read-scoped — never use it to widen. Overriding filter_queryset_for_user needs # karni: allow(access-override): <why>.
Why Karni objects. The queryset is the authoritative layer: lists, details, actions, relation PKs and ?expand= all go through the read scope, so a bypass leaks everywhere.
Fix. Move the condition into access (Where(lambda request: Q(...))), or add the marker.
K111 — Paginate lists; no N+1.¶
agent rule, error · enforced by unpaginated-action, query-scaling
Rule. Lists stay bounded and flat. A list-returning @action returns self.paginated_response(queryset), never Serializer(qs, many=True).data; pagination_class = None needs pagination = Unpaginated(max_rows=N, reason=...). Query counts must not grow with rows: BaseViewSet derives select_related/prefetch_related from the serializer, so hint only what it cannot see (SerializerMethodField, properties) with select_related = [...] / prefetch_related = [...] on the viewset. Raise a budget only with query_budget = Budget(list=N, reason=...).
Why Karni objects. An unpaginated list or an N+1 passes with test data and falls over with real data; the scaling check (2 vs 12 rows) and per-action query budgets catch both in CI.
Fix. Return self.paginated_response(qs); hint opaque fields with prefetch_related.
K113 — Audited models: no update()/bulk_update().¶
agent rule, error · enforced by audited-bulk-write
Rule. A model with class Karni: audit = True keeps field-level change history through save() and delete(); QuerySet.update() and bulk_update() bypass it and leave no entry. Save the instances instead, or opt out with # karni: allow(audited-bulk-write): <reason>.
Why Karni objects. A silent gap in the history is exactly the bug someone discovers during an audit; the rule names every bulk write on an audited model at build time.
Fix. Loop and save(), or add the allow marker with the reason.
K201 — Compose @karni/ui; no raw table/form/input.¶
agent rule, error · enforced by karni/no-raw-elements
Rule. Screens compose @karni/ui components (Page, ResourceTable, ResourceForm, ResourceDetail, ...). Never hand-roll a table or form: no raw <table>/<form>/<input>/<select>/<textarea>.
Why Karni objects. The components already handle data, validation, a11y and theming.
Fix. Compose ResourceTable / ResourceForm / Input / Select from @karni/ui.
K202 — Pages: max 50 lines.¶
agent rule, error · enforced by karni/max-page-lines
Rule. Page files stay at 50 lines or fewer. Past that you are rebuilding something Karni ships — use the component's props.
Why Karni objects. A long page is reimplementing a component.
Fix. Use the components' props; move logic into hooks.
K203 — Import only the @karni/ui barrel.¶
agent rule, error · enforced by karni/no-deep-imports
Rule. Import only the public @karni/ui barrel, never @karni/ui/dist/* or /src/*.
Why Karni objects. Internals change without notice; the barrel is the versioned API.
Fix. Import from "@karni/ui".
K204 — Data only via Karni hooks; no fetch/axios.¶
agent rule, error · enforced by karni/no-direct-http
Rule. Data access only via @karni/ui hooks (useResourceList, useResourceItem, useResourceMutations, the KarniClient) and the typed hooks generated by karni types. No fetch/axios/XMLHttpRequest/WebSocket outside src/api/.
Why Karni objects. The hooks share caching, auth, errors and query-key invalidation.
Fix. Use the generated typed hook or useKarniClient().
K205 — No duplicated frontend code.¶
agent rule, error · enforced by jscpd
Rule. No duplicated frontend code above the frontend/.jscpd.json threshold.
Why Karni objects. Duplicates are where boilerplate creeps back in.
Fix. Extract one component or hook, or compose the existing @karni/ui one.
K301 — Domain code only; extend Karni.¶
agent rule, must · enforced by review
Rule. Karni already implements everything generic; you write only domain code. Extending Karni is always cheaper than reinventing it.
Why Karni objects. Generic code written per project is the boilerplate Karni exists to remove.
Fix.
K302 — Never write auth/users, admin, serializers, routers, settings, infra, tables, forms.¶
agent rule, must · enforced by review
Rule. Never write auth flows, user models, admin registration, serializers, routers, settings, Docker/Compose config, Celery config, HTML tables, forms, or detail views. Karni provides all of these. If you think you need one, you need an override on an existing Karni base instead.
Why Karni objects. Each of these ships in Karni with tested defaults and override points.
Fix. Find the Karni base that provides it (karni context BASES) and override it.
K303 — Extend via props/methods; never fork.¶
agent rule, must · enforced by review
Rule. If an existing component or base class almost fits, extend it through its override props / hooks (columns, fields, slots, AdminMeta, APIMeta, overridable methods). Do not create a parallel variant.
Why Karni objects. A forked variant stops receiving fixes and doubles the surface to maintain.
Fix.
K304 — No extra state/form/table/HTTP/CSS library.¶
agent rule, must · enforced by review
Rule. The stack is fixed. Do not introduce a state library, form library, table library, HTTP client or CSS framework beyond what Karni ships (TanStack Query/Table, react-hook-form, Zod, Tailwind, Radix via @karni/ui). Backend HTTP clients are allowed only in services modules (K105).
Why Karni objects. Every extra library is a second way to do something Karni already does.
Fix.
K305 — Per entity: models.py and viewsets.py.¶
agent rule, must · enforced by review
Rule. Backend code per entity goes in <app>/models.py and <app>/viewsets.py — nothing else. No admin.py, serializers.py, forms.py; urls.py only for non-API endpoints such as webhooks (viewsets ride the auto-router).
Why Karni objects. Admin, serializers, filters, routes and schema are derived from those two.
Fix.
K306 — Pages: frontend/src/pages/.¶
agent rule, must · enforced by review
Rule. Frontend pages live in frontend/src/pages/, one file per page, with the route registered alongside the existing pages.
Why Karni objects. The page cap (K202) and the inventory (karni context PAGES) rely on it.
Fix.
K307 — Cross-project code: a Karni module.¶
agent rule, should · enforced by review
Rule. Anything reusable across projects belongs in a Karni module (a karni_<name> package), not in this project — say so instead of building it.
Why Karni objects. Code that would be written more than once belongs in Karni, never per project.
Fix.
K308 — Modules: configure via settings.¶
agent rule, must · enforced by review
Rule. Configure an installed module only through the settings it documents; never patch its internals.
Why Karni objects. Patched internals break on the module's next release.
Fix.
ESLint rules (@karni/ui/eslint-plugin)¶
Every rule is an error in karni.configs.recommended; an opt-out needs an eslint-disable comment with a -- reason.