File state checkpoint tool - has your code tarnished?
Project description
tarnished
A CLI tool that tracks which quality checks need re-running after code changes.
The Problem
AI coding assistants often forget to re-run tests after making changes:
Timeline What Happens
─────────────────────────────────────────────────────────────────────
1. Modify 5 PHP files
2. Run tests Tests pass
3. Modify 2 more PHP files (to fix a review comment)
4. Say "Done!" PROBLEM: Tests not re-run after step 3
─────────────────────────────────────────────────────────────────────
Result: User discovers broken tests later
This happens because AI assistants lack persistent memory about which files changed after which checks ran.
The Solution
tarnished tracks file state via checksums. Modified files become "tarnished" until their checks pass again.
Action Profile Status
─────────────────────────────────────────────────────────────────────
1. Modify 5 PHP files test:php = TARNISHED
2. Run tests (pass) test:php = CLEAN
3. Modify 2 more PHP files test:php = TARNISHED
4. Run `tarnished status` Shows test:php needs re-running
5. Run tests (pass) test:php = CLEAN
6. Say "Done!" Confident everything works
─────────────────────────────────────────────────────────────────────
Installation
# Using uv (recommended)
uv tool install tarnished
# Using pip
pip install tarnished
# From source
git clone https://github.com/RasmusGodske/tarnished.git
cd tarnished
uv tool install .
Quick Start
# Initialize configuration
tarnished init
# Save a checkpoint after a successful check
tarnished save lint:php "app/**/*.php" "tests/**/*.php"
# Check if files have changed since last checkpoint
tarnished check lint:php
# See status of all profiles
tarnished status
Concepts
┌─────────────────────────────────────────────────────────────────┐
│ PROFILE │
│ A named set of file patterns to track │
│ │
│ Example: "test:php" tracks app/**/*.php and tests/**/*.php │
└─────────────────────────────────────────────────────────────────┘
│
│ when files matching patterns are modified
▼
┌─────────────────────────────────────────────────────────────────┐
│ TARNISHED │
│ Files changed since the last checkpoint │
│ │
│ Action required: Run the associated check │
└─────────────────────────────────────────────────────────────────┘
│
│ when check passes and checkpoint is saved
▼
┌─────────────────────────────────────────────────────────────────┐
│ CLEAN │
│ No changes since the last successful check │
│ │
│ Action required: None - safe to skip │
└─────────────────────────────────────────────────────────────────┘
Profile Statuses
| Status | Meaning | Action |
|---|---|---|
clean |
No files changed since last pass | Safe to skip |
tarnished |
Files changed since last pass | Must re-run check |
never_saved |
Check has never passed | Must run check |
Commands
| Command | Description |
|---|---|
tarnished init |
Initialize .tarnished/ directory |
tarnished save <profile> [patterns...] |
Save checkpoint for profile |
tarnished check <profile> |
Check if profile is tarnished |
tarnished status |
Show JSON status of all profiles |
Exit Codes for check
| Code | Meaning |
|---|---|
| 0 | Clean - no changes since last checkpoint |
| 1 | Tarnished - files have changed |
| 2 | Unknown - profile not found |
File Structure
.tarnished/
├── config.json # Profile definitions (commit this)
└── state.json # Checkpoint data (gitignore this)
Implementation
tarnished fingerprints each matched file at save time (size + mtime + content
hash), then checks like git's index does: files whose size and mtime are
unchanged are trusted without reading; files whose stat drifted are verified
by content hash. Operations that rewrite identical bytes — git pull,
git checkout, code generators — therefore compare clean instead of
producing false "tarnished" reports. When a check verifies drifted files by
content, the stored fingerprints are refreshed so subsequent checks return to
the pure-stat fast path.
- Git-independent - works regardless of commit history
- Honest - changed mtime with identical content is not a change
- Fast - file contents are only read at save time and for stat-drifted files at check time
- Simple - plain JSON files
Integration Examples
Test Script Wrapper
#!/bin/bash
# run-tests.sh
php artisan test "$@"
exit_code=$?
if [ $exit_code -eq 0 ]; then
tarnished save test:php 2>/dev/null || true
fi
exit $exit_code
Pre-commit Hook
#!/bin/bash
# .git/hooks/pre-commit
status=$(tarnished status 2>/dev/null)
if echo "$status" | grep -q '"tarnished"'; then
echo "Tarnished profiles detected. Run checks before committing."
tarnished status
exit 1
fi
CI Pipeline
- name: Run tests if needed
run: |
if ! tarnished check tests; then
npm test && tarnished save tests
fi
Claude Code / AI Assistant
Add to your CLAUDE.md:
Before completing any task, run `tarnished status`:
- If a profile is "tarnished", run that check
- If the check fails, fix it and re-run
- Only complete when relevant profiles are "clean"
Example Configuration
{
"profiles": {
"lint:php": {
"patterns": ["app/**/*.php", "tests/**/*.php", "config/**/*.php"]
},
"lint:js": {
"patterns": ["resources/js/**/*.vue", "resources/js/**/*.ts"]
},
"test:php": {
"patterns": ["app/**/*.php", "tests/**/*.php"]
},
"test:e2e": {
"patterns": ["e2e/**/*.ts", "resources/js/**/*.vue"]
}
}
}
Why "Tarnished"?
Like silver that tarnishes when exposed to air, code "tarnishes" when modified. It needs polishing (running checks) before it's ready. Once checks pass, the code is clean again.
License
MIT
Project details
Release history Release notifications | RSS feed
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 tarnished-0.3.0.tar.gz.
File metadata
- Download URL: tarnished-0.3.0.tar.gz
- Upload date:
- Size: 10.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b1360e76e2b4999c70c4d416a556390069f7fa145dce4621394c1cf795257197
|
|
| MD5 |
4dbbb81dea1aa095626ba0a12b3fdf87
|
|
| BLAKE2b-256 |
e078e795fc64d821ca0abceea9c10bcd16c53a86a7ceb3cffe4925b1414730aa
|
Provenance
The following attestation bundles were made for tarnished-0.3.0.tar.gz:
Publisher:
publish.yml on RasmusGodske/tarnished
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tarnished-0.3.0.tar.gz -
Subject digest:
b1360e76e2b4999c70c4d416a556390069f7fa145dce4621394c1cf795257197 - Sigstore transparency entry: 1739972317
- Sigstore integration time:
-
Permalink:
RasmusGodske/tarnished@bae743661cc9c38bfed711b11c08ab415188864a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/RasmusGodske
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bae743661cc9c38bfed711b11c08ab415188864a -
Trigger Event:
push
-
Statement type:
File details
Details for the file tarnished-0.3.0-py3-none-any.whl.
File metadata
- Download URL: tarnished-0.3.0-py3-none-any.whl
- Upload date:
- Size: 12.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ace6633293bf7c33628c5c53a0ad0eec7dda4bf6b8e9d551c6a46774d0a53c1a
|
|
| MD5 |
b7f74332cb78b7d0ac43438d780d3899
|
|
| BLAKE2b-256 |
7878910823f4ed7cfa24de5aa5ce4992264bc0960812974f76e824550965fca3
|
Provenance
The following attestation bundles were made for tarnished-0.3.0-py3-none-any.whl:
Publisher:
publish.yml on RasmusGodske/tarnished
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tarnished-0.3.0-py3-none-any.whl -
Subject digest:
ace6633293bf7c33628c5c53a0ad0eec7dda4bf6b8e9d551c6a46774d0a53c1a - Sigstore transparency entry: 1739972326
- Sigstore integration time:
-
Permalink:
RasmusGodske/tarnished@bae743661cc9c38bfed711b11c08ab415188864a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/RasmusGodske
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@bae743661cc9c38bfed711b11c08ab415188864a -
Trigger Event:
push
-
Statement type: