Checks that a GitHub repo is in good order — branch protection, CI with tests
and lint, no CI step masking its own failure with continue-on-error, a
scheduled run so bitrot surfaces on its own, bounded job timeouts, no
rerun-until-green test retries, a pinned (non-floating) CI toolchain, a coverage
tool wired up for each language (advisory, presence-only), dependabot
coverage, secret scanning, read-only workflow tokens, lockfiles committed and in
sync, gitignore coverage, CODEOWNERS routing review, shell scripts corralled
under scripts/ with a dev.sh that stands up the dev environment, TODO/FIXME
markers kept in the todo file rather than scattered,
straitjacket wired into CI, stylelint
on stylesheets, vale on prose style and codespell on typos, a README that clears
the floor, a reachable website, a license, sane repo metadata, and no stale PRs
or branches. One repo at a time; run it when you touch a repo.
Checking is always read-only. Fixing is separate, explains itself, and asks
before changing anything; file fixes land on a housekeeping/<check> branch
and only push + open a PR on an explicit yes.
The defaults are my interpretation of what good code looks like, whether
it's public or private — snobby yet configurable, in the family spirit of
straitjacket. Public repos get the
full audience-facing treatment; private repos soften those checks (website,
license, changelog, README polish, metadata), because a repo with no audience
doesn't owe anyone a changelog. Engineering hygiene — CI, lockfiles,
dependabot, secret scanning — is required either way. .housekeeping.toml
overrides any of it per repo.
uv tool install git+https://github.com/zmaril/housekeeping
# or from a checkout:
uv tool install .Needs gh (authenticated) and git. Lockfile sync checks use whichever of
cargo/bun/npm/uv/… are installed and degrade to presence-only otherwise.
Run the audit in your own repo's CI — on PRs, pushes, and a weekly cron for the things that rot without commits (dead links, stale PRs, settings drift):
name: housekeeping
on:
push:
branches: [main]
pull_request:
schedule:
- cron: "0 7 * * 1"
jobs:
housekeeping:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: zmaril/housekeeping@v0.21.0 # pin the full version — newest is on the Releases pagePin the full version, not a moving major tag. Housekeeping adds checks in
minor releases; a floating @v0 would apply every new check to your repo the
moment it ships, turning an unrelated PR red with something you never opted
into. Pin the exact release (or a commit SHA) so new checks arrive only when you
deliberately bump — on a PR that's about that upgrade, where you can read the
changelog and decide. Dependabot/Renovate can raise those bumps
for you. The newest release is on the
Releases page. Results land in
the job summary; a required-check failure fails the run.
The default workflow token can't read some admin-level settings
(vulnerability alerts, secret scanning, workflow permissions) — those checks
skip with a note rather than guessing; pass with: token: a fine-grained
PAT with read-only Administration scope for full coverage. Tune or disable
checks with .housekeeping.toml in your repo root.
Repos audit themselves; the captain checks the auditors. A captain repo
carries a housecaptain.toml naming the fleet:
name = "powderworks"
[[member]]
repo = "zmaril/housekeeping"
[[member]]
repo = "zmaril/entl"
note = "pre-release, in flux"
[policy.checks]
conventional-commits = "required"
[policy]
locked = ["checks.stray-files", "stray-files.allow"]Policy is expectation (divergence surfaced as a conflict) unless locked:
members declare fleet = "owner/repo" in their .housekeeping.toml, their
own audits fetch the manifest and enforce locked keys as law — a PR that
sets a locked key fails its own CI, so nobody excepts themselves in the same
diff that adds the mess. Locking requires a top-level captain = "owner/repo"
in the manifest; the captain flags members that don't declare their fleet.
Adoption note: the PR that introduces locks to the manifest will have a
red captain check, by construction. The captain reads member configs from
their main branches — so members that haven't merged their fleet = ...
lines yet show as conflicts, and the captain repo itself conflicts on the
very PR that adds its own declaration (it can't bless its own enrollment).
Merge order: member fleet-lines first, then the manifest PR — merged red on
that one self-referential conflict — and the push-triggered captain run
right after merge is the real verdict.
housekeeper captain (or the action with captain: housecaptain.toml) is
the API-only delegation check: each member has a housekeeping workflow, it
fires on pull_request + push + schedule, its latest default-branch run is
green, and the member's .housekeeping.toml doesn't contradict fleet
policy — divergence is surfaced as a conflict for a human to reconcile,
never silently resolved. housekeeper fleet is the deep version: the full
audit against every member from your machine (members audited concurrently),
with a scoreboard. --html FILE writes a standalone dashboard — the check
matrix plus a table of every open PR and issue across the fleet. And
housekeeper captain --dispatch (action input dispatch: true) is the
fleet's "now" button: it triggers every member's self-audit immediately, so
a new check reaches everyone without waiting out the weekly crons.
housekeeper serve housecaptain.toml puts the same dashboard behind a local
web server with a Regenerate button (and an optional 60s auto-refresh)
that re-audits the fleet on demand — no re-running the command by hand.
Some checks pass on presence but the config's content is a fleet decision —
everyone should lint CSS by the same rules and spell-check against the same
vocabulary. The captain can own the canonical file and push it outward.
Canonical configs live under .fleet/ in the captain repo, declared in the
manifest:
[[policy.managed-config]]
check = "stylelint"
paths = { ".stylelintrc.json" = ".fleet/stylelintrc.json" }
[[policy.managed-config]]
check = "vale" # trailing-slash keys are directory syncs
paths = { ".vale.ini" = ".fleet/vale/.vale.ini", "styles/" = ".fleet/vale/styles/" }housekeeper captain --sync-configs (action input sync-configs: true) opens
an isolated, config-only PR on each member whose copy has drifted — branch
housekeeping/fleet-config-<check>, reused idempotently, titled chore(config): sync <check> config from fleet. This is distribution, not a fix: the captain
ships the artifact for the member to adopt at their own pace and never
touches member code or tries to make the resulting lint pass — that stays the
member's own, one-repo job. Drift shows on the captain report as a note, never
as a member-CI failure. It needs a token with contents:write +
pull_requests:write on the members.
Wire it to fan out on merge with a workflow in the captain repo:
name: fleet-sync
on:
push:
branches: [main]
paths: ['.fleet/**'] # only when a canonical config changes
schedule:
- cron: "0 7 * * 1" # backstop: new members, PRs closed unmerged
workflow_dispatch:
concurrency: { group: fleet-sync }
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: zmaril/housekeeping@v0.21.0 # pin the full version
with:
captain: housecaptain.toml
sync-configs: true
token: ${{ secrets.FLEET_PAT }}The default workflow GITHUB_TOKEN can only write to the captain repo, so sync
needs a separate token stored as the FLEET_PAT secret. Create a fine-grained
PAT with Contents: read/write and Pull requests: read/write, and — the
sharp edge — grant it access to every member repo. Fine-grained PATs are
repo-scoped: a member missing from the token's repo list fails with a silent
HTTP 403 and simply never gets its sync PR, while the rest sync fine. So
whenever you add a member to the manifest, add it to the FLEET_PAT too.
When a sync run reports error: the sync token can't write to <repo> (HTTP 403),
that member is the one missing from the token.
The tidy-up skill audits the repo you're in, drives fixes, and does a
README quality pass. Install it into Claude Code as a plugin:
/plugin marketplace add zmaril/housekeeping
/plugin install housekeeping@powderworks
then invoke /housekeeping:tidy-up. Or install it into any agent
(Claude Code, Cursor, Copilot, …) via skills.sh:
npx skills add zmaril/housekeepingEither way, the skill offers to install the housekeeper CLI if it's missing.
housekeeper check # repo inferred from cwd's git remote
housekeeper check zmaril/entl # or named explicitly
housekeeper check --only lockfiles,branch-protection
housekeeper fix dependabot # explain, confirm, then act
housekeeper report # re-render the last run
housekeeper detect # show detected ecosystems, artifacts, recommended setup
housekeeper new my-lib --flavor rust # scaffold a fleet-compliant repo skeleton
housekeeper new my-lib --dependabot-automerge # ...and opt into dependabot auto-mergecheck exits nonzero if any required check fails, so it can gate other
automation. Results are saved as JSON under ~/.cache/housekeeping/results/.
Drop .housekeeping.toml in a repo root to override defaults:
[checks]
website = "off" # required | recommended | off
[website]
url = "https://straitjacket.dev" # expected homepage
[allow-auto-merge]
enabled = true # true | false (default false)
[pinned-versions]
cargo = "advisory" # advisory (default) | on | off
actions = true # scan uses: refs (default true)
capped_ok = false # accept bounded ranges (>=1.7,<2.0, ~>) as pinned
ignore = ["@typespec/compiler"] # dependency / action names to skip
[[codegen]] # committed generated code: CI must regen + zero-diff
name = "ruby bindings"
command = "make bindgen"A repo picks its auto-merge policy with [allow-auto-merge] enabled = true
(or false, the default) in .housekeeping.toml. The allow-auto-merge
check then verifies GitHub's actual setting matches the declared preference
either way; housekeeper fix allow-auto-merge flips the GitHub setting to
match (needs an admin token — a 403 prints how to flip it by hand).
Add dependabot = true to the same table to opt into the dependabot-automerge
check: it wants a workflow that turns on auto-merge for dependabot's own PRs
(non-major bumps), so GitHub lands them the moment their required checks go green.
It needs enabled = true too, and only actually gates on green CI when the repo
has required status checks registered; housekeeper fix dependabot-automerge
writes the workflow.
The repo-meta check reads its declaration not from .housekeeping.toml but from
the README itself, via two invisible HTML-comment markers at the top of the file:
<!-- housekeeper:description One-line tagline goes here. -->
<!-- housekeeper:topics rust, cli, codegen -->The README is the source of truth: the check asserts GitHub's actual description and
topics match what the markers declare (topics are lowercased and must be
[a-z0-9-], ≤50 chars each, ≤20 total — GitHub's own rules). housekeeper fix repo-meta pushes the README-declared values to GitHub (needs an admin token — a 403
prints how to set them by hand); when a repo has no markers yet, the same fix seeds
them into the README from GitHub's current description/topics, so adopting the
convention is one fix away.
pinned-versions (recommended) flags floating version specifiers per detected
ecosystem so a dependency can't silently drift: npm/bun and python and ruby want
an exact version (1.2.3, ==1.2.3, = 1.2.3), actions want a 40-char commit
SHA, and cargo is advisory because Cargo.lock pins the build. @stable /
@oldstable action channels are allowed (matching reproducible-toolchain), and
local/path/SHA-pinned deps are never flagged. Set [pinned-versions] cargo = "on"
to enforce cargo, actions = false to skip workflows, capped_ok = true to accept
bounded ranges (>=1.7,<2.0, ~>), or ignore = ["actions/checkout"] to skip
names. It never fixes automatically — pinning to a version is a human call, and
dependabot still bumps the pins afterwards.
Private repos automatically soften website and license to recommended,
and branch protection reports skip-with-note where the plan doesn't allow it.
skills/tidy-up/ is the skill source — front door for the audit, fix
driving, and the README judgment pass the deterministic check can't do.
Working on housekeeping itself? Symlink it into ~/.claude/skills/ instead
of installing the plugin.
See notes/design.md.
Issues and PRs welcome. The most useful thing you can send is a repo where housekeeper judges wrongly — a check that passes when it shouldn't, fails when it shouldn't, or a skip that deserves a better note. Concrete examples beat descriptions.
Clone, then run ./scripts/dev.sh — it syncs the uv environment and wires up
the committed git hooks, so you're ready to test, lint, and audit in one step.
New checks are one module in src/housekeeper/checks/ — see the check
contract in notes/design.md. House rules: checks are read-only, fixes
explain themselves and confirm before touching anything, and skips say why.
PR titles follow conventional commits
(type(scope): summary) — CI enforces it, and squash merges inherit the
title, so main's history stays machine-readable.
uv run pytest and uv run ruff check . before pushing; CI also runs
straitjacket on everything,
prose included.
MIT.