Owner: the founder. Approved: 2026-10-08. Review: annually.
Scope: every line of code that reaches production, and every tool that decides
whether it may. Change process (branches, PRs, deploys, rollback) is
change-management.md; this policy is about the code itself.
mypy --strict runs on src and tests in
the pre-push gate. It is deliberately not in the commit hook — about five
minutes on a fresh worktree with no cache (measured 2026-10-08) — and can be
run there by hand as a manual-stage hook. A function without types does not
get pushed.ruff runs with S
(flake8-bandit) selected, so the checks a standalone Bandit run would make —
subprocess without a fixed argv, assert in shipped code, hardcoded
credential literals, unsafe deserialisation, weak hashes — fail the build
instead of being reported in a file nobody opens. T20 bans print():
nothing reaches a customer's log line except the structured logger.| safe filter is used only on copy we author, never on customer text.ruff's TID
banned-imports rule enforces it.docs/security/secrets.md; the enforcement is
two scanners, below.pyproject.toml and locked
in uv.lock. Adding a vendor (a service, not a library) is the founder's
decision, not the implementer's.| Gate | Where | What it fails on |
|---|---|---|
| file hygiene (whitespace, end of file, merge markers, YAML/TOML syntax, files over 500 KB, line endings) | commit hook | the staged files |
ruff check src tests |
commit hook + pre-push gate (lint) |
style, bugbear, security (S), banned imports |
mypy --strict src tests |
pre-push gate (types) |
any untyped or wrongly typed symbol |
pytest |
pre-push gate (unit) |
any failing test, including the guard tests below |
gitleaks |
commit hook (staged diff) + pre-push gate (secrets, the branch's commits) |
a credential in the diff |
detect-private-key |
commit hook | a PEM block in the diff |
gitleaks (full history) |
pre-push gate (history) |
a credential anywhere in the history |
trivy fs |
pre-push gate (dependencies) |
a CRITICAL advisory in a dependency or a CRITICAL Dockerfile misconfiguration (HIGH is reported, not gated) |
| api fuzz | pre-push gate (api-fuzz) |
a 5xx or an unhandled exception on a fuzzed public surface |
| prompt-injection probe | pre-push gate (unit) |
a known injection string reaching a tool call (hermetic corpus lint; the paid live run is manual) |
| migration chain | pre-push gate (database) |
a migration that will not upgrade, downgrade and re-upgrade — and a final step that proves RLS still bites as the runtime role, not as the owner |
The commit hook is scripts/git-hooks/pre-commit, which runs
.pre-commit-config.yaml on the staged files in seconds. The pre-push gate is
scripts/check.py, run by the git pre-push hook; it refuses the push unless
every stage is green, and a pull request is merged only after the gate is green
on its head (ADR-0119). Both are installed once per clone by
scripts/install_git_hooks.sh, and tests/test_git_hooks.py fails if a hook
these documents name is missing from scripts/git-hooks.
What these gates are not: an independent witness. They run on the machine the
change was written on, --no-verify skips them, nothing on GitHub records that
they ran, and there are no required status checks — GitHub branch protection is
not available for a private repository on the free plan. "Merged only after the
gate is green" is the founder's discipline, not a setting.
These are the architectural promises this product is sold on. Each has a test that turns the suite red when the promise is broken, which is why they are controls and not slogans:
tests/test_rls_coverage_all_tenant_tables.py). A new table without
one fails the pre-push gate before it can hold a row.SELECT, INSERT on
audit_log and had UPDATE, DELETE revoked; the grants are asserted, not
assumed.tests/test_i18n_locale_key_parity.py).src/ataski/trust/controls.py that the
public page does not render, and a written policy in this directory with no
control behind it, both fail tests/test_trust_controls.py. This document is
covered by that test.Dependencies are pinned and locked; trivy fs reads the lock file before every
push. Upgrades inside a semver-compatible range are made autonomously; a
major-version upgrade is a founder decision. There is no automated dependency-
update bot configured today — see §6.
A CRITICAL advisory in a dependency gates the push, so it
is fixed or explicitly waived in .trivyignore with a reason and a date
(tests/test_trivy_ignore_policy.py refuses an entry without either). A HIGH
advisory is reported on the run and swept in batches. A vulnerability found in
our code is handled as an incident when it is exploitable in production, and
as an ordinary fix when it is not — the deciding question is whether customer
data could have been reached, and it is answered in writing either way.
.github/dependabot.yml exists; upgrades are manual and
prompted by the trivy report.CLAUDE.md and measured on demand; the pre-push gate does not fail on a drop.ruff's security ruleset. Semgrep is not run. ruff S
covers the Bandit checks; the interprocedural rules Semgrep would add are not
in place.docs/security/soc2-readiness.md rather than papered
over.trivy image needs Docker, which the gate machine does not have). The
quarterly review runs trivy fs by hand to cover the first half.