keep-the-why-lint
Keep the Why preserves the reasoning behind your code. keep-the-why-lint checks that it's recorded in a form the next reader can rely on.
keep-the-why-lint is the structural CI linter for Keep the Why projects — the tests for your context/. Keep the Why is a repo-native convention and agent skill for preserving the reasoning behind a codebase: decisions, rejected alternatives, workarounds, incidents, constraints — the why that code alone can't explain, stored as versioned Markdown in context/. Nothing in that format is enforced the way a compiler enforces correctness. Part of the gap is mechanically closable, though, and that part is this tool's job: required fields, valid values, index consistency, .keep-the-why integrity, hidden-content red flags.
Schema-version-aware: it reads the target project's context-schema and only enforces what that skill version defines — the same "next time touched" philosophy as the skill's own migrations. An unmigrated project never fails on structure its version didn't have.
Runs on: Python 3.10–3.14, no dependencies beyond the standard library — anywhere pip works: GitHub Actions, GitLab CI, pre-commit, or the shell.
Website: https://keepthewhy.com · llms.txt for AI agents/assistants looking up this project
Documentation: Linting · CI linting setup · Finding codes · Migrations · Trust model
How it works
ktw-lint reads .keep-the-why, finds the configured context directory, and validates the machine-checkable half of the schema:
- Entries — every entry carries
StatusandEvidence, values come from the documented sets,Typeis valid,Verification: contradictedsays what contradicts it index.md— exists, every link resolves, every topic file is listed, sorted alphabetically.keep-the-why— required fields present, no field recorded twice, no unknown fields, pinned versions consistent, the configuredcontext/location exists- Hidden content — invisible or directional Unicode is an error, base64-looking blobs a warning: the one mechanically checkable slice of the trust model
Every check is gated by the project's context-schema, so a check only fires for a skill version that actually defined it. Fenced code blocks are skipped — example entries in documentation never get linted as real ones.
Exit code 0 clean, 1 findings, 2 usage error. Inside GitHub Actions (GITHUB_ACTIONS set, or --github) findings are emitted as ::error/::warning annotations, so they show up inline on the PR.
Install
pip install keep-the-why-lint
ktw-lint . # lint the project in the current directory
ktw-lint /path/to/project # or any other project root
ktw-lint . --strict # warnings fail too
CI setup
The Keep the Why project init wizard offers to wire the linter into your CI during setup — detected from the repository, never guessed — and CI linting setup has the full detection rules. By hand, these are the same snippets:
GitHub Actions — .github/workflows/ktw-lint.yml. The root of the keep-the-why repository is a composite action (on the GitHub Marketplace) that installs the latest linter from PyPI; the lint-latest tag moves with every linter publish, so there's nothing to pin on your side (pin @lint-v<version> if you want a fixed action revision):
name: ktw-lint
on:
push:
branches: [main]
pull_request:
jobs:
ktw-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oliver-zehentleitner/keep-the-why@lint-latest # or @lint-v<version> to pin the action
with:
path: "."
strict: "false" # "true" turns warnings (e.g. missing Type on old entries) into failures
# version: "0.10.1.0" # optional: pin the linter itself; @lint-v<version> above pins only the wrapper
GitLab CI — job for .gitlab-ci.yml:
ktw-lint:
image: python:3.12
script:
- pip install keep-the-why-lint
- ktw-lint .
pre-commit — hook for .pre-commit-config.yaml (the skill repository root isn't a Python package, so the hook pulls the linter from PyPI):
repos:
- repo: local
hooks:
- id: ktw-lint
name: keep-the-why-lint
entry: ktw-lint .
language: python
additional_dependencies: ["keep-the-why-lint"]
pass_filenames: false
strict: "false" / no --strict is the sensible default: warnings (a missing Type on an old entry, a missing guard file) are "next time touched" material per the skill's own rules and shouldn't block a fresh project's CI.
Example
A run against a project with a few problems:
context/auth.md:3: [E102] 'Token refresh window': entry has no **Evidence:** field
context/index.md: [E203] topic file 'auth.md' is not listed in index.md
context/sync.md:11: [W101] 'Retry budget': no **Type:** field — fill it in the next time the entry is touched
context/sync.md:13: [E103] Status 'actve' is not one of: active, superseded, open, needs-review
ktw-lint 0.10.1.2: 3 error(s), 1 warning(s) (context-schema 0.10.1, context: context/)
Every finding code, with its meaning and severity: Finding codes.
Version scheme
Versioned as <schema>.<revision> — e.g. 0.10.1.0. The first three segments are the newest skill schema this release knows every structural gate of; the fourth is the linter's own revision, bumped for linter-only changes (0.10.1.1). A skill release without structural changes doesn't need a linter release: the gating handles newer context-schema values, and W003 warns when a project's schema is newer than the newest one the linter knows, so you can check migrations for whether an update matters.
PEP 440, not strict SemVer — PyPI rejects the build-metadata spelling SemVer would use for this. Releases are tagged lint-v<version> in the repository, and the moving lint-latest tag — the ref the GitHub Action snippet uses — follows the newest one; both are created by the publish workflow only after a successful upload, never by hand.
What this is not
- Not a content checker. Whether the recorded rationale is true, complete, or honest is not mechanically checkable, and this tool doesn't pretend otherwise —
Evidence: confirmedstays a human judgment; the linter only guarantees the field is there and holds a legal value. - Not a secret scanner. The hidden-content check catches what needs decoding to be read; pair it with a real scanner (e.g. gitleaks) if you need that.
- Not a substitute for the skill. It validates what Keep the Why writes; it doesn't write anything itself — see Installation for the skill.
Feedback
Something not working as described, a finding that's wrong, or a structural rule the linter should know about? Open an issue — that's exactly what it's for.
Contributing
Developed in the keep-the-why monorepo under lint/, released independently of the skill. See CONTRIBUTING.md, the Changelog, and the Security policy.
Contributors
We ♥️ open source!
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keep_the_why_lint-0.11.0.0.tar.gz.
File metadata
- Download URL: keep_the_why_lint-0.11.0.0.tar.gz
- Upload date:
- Size: 19.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
525d698e6d06861fbf68a04be770153cb4cce9d1b27182e53573f4f31d09f7c5
|
|
| MD5 |
160a645045f00aa73657e5758e6eac73
|
|
| BLAKE2b-256 |
b81ef99be96abc38985a35eab37b8809fe16a0c0622b3fdf2f2783470bf749e8
|
Provenance
The following attestation bundles were made for keep_the_why_lint-0.11.0.0.tar.gz:
Publisher:
publish-lint.yml on oliver-zehentleitner/keep-the-why
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keep_the_why_lint-0.11.0.0.tar.gz -
Subject digest:
525d698e6d06861fbf68a04be770153cb4cce9d1b27182e53573f4f31d09f7c5 - Sigstore transparency entry: 2700857624
- Sigstore integration time:
-
Permalink:
oliver-zehentleitner/keep-the-why@cd20a4a0a760f0fb746eda6555f619d280e88378 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/oliver-zehentleitner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-lint.yml@cd20a4a0a760f0fb746eda6555f619d280e88378 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file keep_the_why_lint-0.11.0.0-py3-none-any.whl.
File metadata
- Download URL: keep_the_why_lint-0.11.0.0-py3-none-any.whl
- Upload date:
- Size: 17.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f78f28a86702868442bf2543a93e50d1b02315333c45a2ac82024e7b70ac9730
|
|
| MD5 |
2c9f074ee6cd928078ea7134e13083cf
|
|
| BLAKE2b-256 |
795dc9076559580fb2a844e6220f551f6d9962fbdd30585020788aaf7fd454c6
|
Provenance
The following attestation bundles were made for keep_the_why_lint-0.11.0.0-py3-none-any.whl:
Publisher:
publish-lint.yml on oliver-zehentleitner/keep-the-why
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
keep_the_why_lint-0.11.0.0-py3-none-any.whl -
Subject digest:
f78f28a86702868442bf2543a93e50d1b02315333c45a2ac82024e7b70ac9730 - Sigstore transparency entry: 2700857667
- Sigstore integration time:
-
Permalink:
oliver-zehentleitner/keep-the-why@cd20a4a0a760f0fb746eda6555f619d280e88378 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/oliver-zehentleitner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-lint.yml@cd20a4a0a760f0fb746eda6555f619d280e88378 -
Trigger Event:
workflow_dispatch
-
Statement type: