agentleFS
w2urw2ur/claude-code-setup/skills/scheduled-jobs/SKILL.md

scheduled-jobs

Skill2 starsChanged 2 months agoMITView on GitHub

Why each launchd LaunchAgent on this machine exists, scheduled at the hour it is, and which plausible "fixes" are wrong. Load before creating, editing, moving or diagnosing any scheduled job. Current state comes from jobs-inventory.sh, never from this file.

  • Reads credentials
  • Commits and pushes

6 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

2 rules. 0 are common, 1 is distinctive, and 1 is very specific to this project.

  • 0

    Common

    Most of the 6 edited versions we read keep these.

  • 1

    Distinctive

    This author's own choices. Worth a look.

  • 1

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Current state is derived, never written here: Run ~/.claude/scripts/jobs-inventory.sh (add --json) for the live list: label, schedule, …

    line 9
  2. Distinctive:

    Do not "tighten" the guard back.

    line 286

“Common” is measured: kept by at least 6 in 10 of the 6 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Scheduled jobs — the reasons

Current state is derived, never written here. Run ~/.claude/scripts/jobs-inventory.sh (add --json) for the live list: label, schedule, program, log, log age, last exit. This file holds only what cannot be derived — the judgment behind each job. Never add a count, a roster, or a file list to this file. Every count previously kept in prose here drifted.

Exit convention, portfolio-wide: 0 = healthy · 1 = a finding · 2 = could not run = unknown, never "healthy" and never "nothing to do". A mounted-but-empty volume, an empty catalogue, an unauthenticated API returning zero rows — all are unknown.

Cross-cutting rules

Never schedule anything that reads the login keychain from crontab. Four instances, each found the hard way: claude's OAuth credentials, gh's OAuth token (~/.config/gh/hosts.yml carries no oauth_token: line), an IMAP password, and the Elenchus status secret. A crontab job runs outside the GUI login session, so login.keychain-db is absent from its search list. The failures are silent, not loud: claude reports Not logged in; gh returns HTTP 401; the security lookup returns an empty string with no error, so the job reports "nothing new" forever. Remedy is a LaunchAgent in ~/Library/LaunchAgents/, which runs inside the GUI session. Do not "fix" it by adding USER= — real cron already sets it; that is a red herring that mimics the PATH bug below.

Every plist carries its own EnvironmentVariables.PATH. launchctl getenv PATH returns nothing here, so a plist without it inherits launchd's built-in /usr/bin:/bin:/usr/sbin:/sbin. Real dependencies: node/npm from fnm's ~/.local/share/fnm/aliases/default/bin — always the aliases/default symlink, never a node-versions/<v> path, which one fnm install re-arms; claude from ~/.local/bin; gh and uv from /opt/homebrew/bin. jq and curl are Apple's in /usr/bin and need nothing. fnm's node is v24, Homebrew's is v26, and Vercel caps at 24 — falling through to the Homebrew binary builds on a version the deploy platform refuses. Check which -a before asserting a binary needs Homebrew.

Nothing scheduled here calls a bare python3. A standalone script carries a PEP 723 header and a #!/usr/bin/env -S uv run --script shebang, and the plist executes the file directly so that shebang selects the interpreter. Passing the script as an argument to an interpreter bypasses the one place its dependencies are declared.

A binary guard does not cover a project's installed dependencies. Guards check node, not node_modules. A 2026-08 disk sweep deleted 16 repos' node_modules; every guarded binary was still present, so vigie-refresh ran, regenerated snapshot.json, then died in the build step that then existed. The visible result was the worst shape available: a fresh snapshot behind a frozen page, answering with numbers that looked current. The build is gone, but the lesson is not vigie-specific. Before deleting a dependency directory, list which scheduled jobs build from that repo.

When a script's header states a cadence, check that something fires it before believing the cadence. Three jobs have been found describing themselves as scheduled while nothing triggered them, distribution-watch among them. A script that has never run leaves nothing behind to notice, and a status file records only runs that happened.

When you move a scheduled job, the trigger is the deliverable, not the prompt. A status.json records runs that happened, never runs that were never triggered — so a missing trigger is indistinguishable from a quiet week. After any move, fire the job once and read its log before calling the migration done.

Wrong beliefs a future session will re-derive on its own

An assumed quota-reset date is not a measured one. The 2026-08-18 session recorded 2026-09-17 as the reset; the real reset was 2026-09-01, found only by watching a previously quota-blocked run turn green. Also: a public repo is off the included-minutes meter entirely, so a repo flipping public silently removes it from any burn projection.

Do not grant Full Disk Access to uv. TCC's kTCCServiceSystemPolicyAllFiles table contains no row for uv at all — the grant has never existed — and devlog-collect runs to exit 0 in ~10 s writing onto the Drive. rclone (the Drive mirror job) reads the same mount with no TCC row either. A hang on a Drive path is the FileProvider not yet materialising, not a missing grant. Note claude carries an explicit FDA denial (auth_value 0) and still reaches the Drive mount.

A bash script launchd starts itself cannot list iCloud Drive; start it through icloud-host.py. Measured 2026-11-07 with one-shot LaunchAgents: ls and rsync on an iCloud directory get "Operation not permitted" when /bin/bash is the process launchd started, and succeed when uv's Python is (the host runs the script as its child). Writing to a known path still works from bash — db-backup's mv lands every night — so a job that only writes can look fine while one that lists fails. Do not "fix" it by granting Full Disk Access to /bin/bash: that hands every bash job full disk access.

Per-job judgment

tech-debt-triage — Phase 2 deep-review selections stay manual. Items flagged 2+ months without action escalate L1→L2→L3 to troubleshooter.

cleanup — runs cleanup-cron.sh, not claude -p "/cleanup". /cleanup commits at Step 1, can fire /sync-setup at Step 4, and its CONFIRM tier is interactive by design — none of that is safe unattended. The wrapper covers Step 0's AUTO tier only and logs the rest as owner action:.

usage-watch — guards on curl and a uv-managed Python with a fatal exit rather than a skip, so a missing interpreter cannot be mistaken for a quiet week. Its findings, payload growth included, notify through notifier.sh itself: vigie drops exit 1 on the assumption that the job already reported it. A run that measured nothing (no route list, zero routes, every probe failing at the transport level, the ratchet crashing) is exit 2, never a finding. Hosts that answer non-2xx or catch-all are findings, even when every one does. So is a Vercel token the API rejects (401/403, or a JSON error body): a dead discovery must not read as a quiet week. The baselines follow the rules written at the top of the script's ratchet. A route still listed but not measured keeps its entry, because dropping it would reset that route's growth ratchet. Only a carried entry written before measured_at existed gets measured_at: null, never an invented date; a carried entry that already has a date keeps it. An ignored host's entry is dropped, because nothing will ever refresh it. A route is dropped only when it verifiably left the overlay, or when discovery answered without it: a failed discovery, or a run where every probe failed at the transport level, prunes nothing discovery could have produced.

model-watch — a model chain hides its own degradation, and a delisted entry is worse than a degraded one: OpenRouter validates the whole models array up front, so one stale entry 400s a request the primary could have served. Refuses to read an empty catalogue as everything having been delisted. Two discovery sources, because one missed an outage: wrangler.toml chains, and url:/model: literal pairs in Netlify functions — my-bias-app's Groq model sat in a .ts constant, was retired on 2026-08-16, and 502'd for two weeks unseen. Groq has no public free catalogue, so its ids are checked against the public deprecations page, reading only the first <code> of each table row — the third column is the replacement, and a whole-page substring grep flagged the replacement as retired on its first run. Its falsifying control is that the retired set still contains llama-3.3-70b-versatile; otherwise exit 2. Here-strings, not pipes, for the membership tests: under pipefail, grep -q closing the pipe on its first match gives printf a SIGPIPE on a large string, and that "failed" pipeline reads as not found — a false negative, the silent kind. Listed is not served. On 2026-10-22 a chain entry stayed listed while its only provider sat at 0% uptime for a day, every Elenchus call ended on its 404, and this job said "ok". Each OpenRouter entry is now also read from /models/<id>/endpoints: no provider up is DEAD (a finding), an unreadable answer is UNKNOWN (exit 2). An id without :free is a deliberate paid fallback and only has to be listed. It still cannot see a saturated free pool (429s): only the proxy's [upstream] log can.

devlog-collect — the plist executes collect.py directly so its uv run --script shebang selects the interpreter. See the FDA note above.

graphe — daily build of the writings graph and the corpus index (my-editor-app/tools/graphe, my-editor-app/tools/index), via graphe.sh. Daily since 2026-11-01 so a text reaching final, a newer .pages or a published post reaches the index the next morning; the index costs no model call, and the graph re-extracts only changed texts. 08:47, not 08:37: 08:37 is held every day by another agent, and 08:47 is clear of the other jobs (checked against jobs-inventory.sh on 2026-10-27, and for every weekday on 2026-11-01). The indexer needs Node 24 by path and two installed dependency trees — my-editor-app/node_modules (the stemmer) and my-editor-app/tools/index/node_modules (the model runtime); the script checks both and exits 2 without them, so list this job before deleting either. LaunchAgent because the tool calls claude -p (OAuth in the login keychain) and reads the Google Drive File Provider mount, which exists only in the GUI session. The per-run extraction cap lives in the script, not the plist: an unattended run must be cost-bounded, and a backlog shows up as differes in the journal and drains over the following runs. Notifies through notifier.sh only on exit 1 or 2, with journal counts as the body, never a path or a citation (the topic is public). Every run, success included, ends with one timestamped summary line so the log's age stays a liveness signal. The script owns the number and the paths.

portrait — weekly consolidation of the owner portrait (~/.claude/portrait.md) from the week's auto-memories, via portrait-consolide.sh. It proposes and never writes the portrait: the portrait is imported into every session, so one wrong fact misleads all of them at once, and an unattended model must not be the one to put it there. It leaves a proposition file; the owner applies or dismisses it from /portrait, which runs portrait-check.sh after the write and restores the old text when the check refuses. Sunday, after graphe: the week's memories are closed, and the two model calls do not start in the same minute. LaunchAgent for the same keychain reason as every claude -p job. The window, the pinned model and the call timeout live in the script; nothing calls the model when no memory is recent. Notifies through notifier.sh only on 1 or 2, counts only, never a proposed text.

setup-watch — weekly health sheet for the Claude Code setup itself, via setup-watch.sh; it proposes and applies nothing. It watches three things, each because it fails without a sound: the login expiry (an expired OAuth login turns every claude -p job into Not logged in at once), the unreviewed CHANGELOG versions (upstream changes land in settings.json, hooks and frontmatter semantics without anyone being told), and the scheduled jobs failing. LaunchAgent for the keychain reason, as every claude caller. No model in v1: it is a delta and a list, and the model stage is revisited only after a few sheets have been treated by hand, together with a generic propositions tab — deferred because the verbs do not share a meaning (a portrait edit is mechanical, a setup item is session work) and the sheet format is known to be provisional. The CHANGELOG, not the RSS: the RSS drops every Fixed line, and those are the ones that explain a behaviour that changed under a hook. Versions are skipped upstream, so membership is by exact heading line, never by number, and the marker's own heading is a falsifying control: absent from the fetched body, the section is unknown (exit 2), never "everything is new". The marker advances only through --traite, run by the owner from /setup-watch: a marker that advances by itself either marks unread versions as reviewed or grows without bound. News alone is exit 0: a weekly exit 1 on routine releases teaches the reader to skip it, and vigie drops exit 1. Failures are keyed on first sighting in the job's own state, not parsed from heterogeneous logs, so the age is "at least", never the failure's real start. An unknown launchd exit (-, (never exited)) after a reboot or reload keeps the first-seen date; dropping it would reset the age at every reboot and hide a long failure. Two exemptions on exit 1, for different reasons: portrait (1 means a proposal was written, not a failure) and setup-watch itself (its 1 restates findings already listed, so counting it makes every finding re-report itself the following week). The marker lives in state/, which is not backed up: losing it gives exit 2 repère absent until re-seeded, the honest failure. The script owns the threshold, the timeouts and the item cap.

vigie-refresh — its script lives in a project repo, like devlog-collect's and the tool graphe.sh launches, so /sync-setup covers none of them and each runs whatever branch that checkout has. It collects the snapshot and then pushes what changed: since 2026-09-13 vigie has no panel, so this agent is the whole of it. Guards on node/npm/git only, fatal exit 2: without those no snapshot exists at all. gh and uv are deliberately NOT fatal. Each feeds exactly one collector, and runCollectors isolates a collector that throws — a missing gh records {ok:false}, the columns render SIGNAL LOST, and the band names the missing source. That is the designed, visible, one-column failure. Making them fatal was tried and is strictly worse: the script exits before the collect, so nothing regenerates the snapshot and every push is computed from a stale one — every source ok:true and nothing wrong, under a generated_at nobody reads. A guard written to prevent a plausible-looking answer nobody measured was manufacturing one. It cds into the repo first, deliberately: the collectors resolve .env, the snapshot and the previous snapshot against the cwd, and a scheduled job runs with cwd $HOME.

It calls notify.mjs and must read three exit states. Under set -euo pipefail a bare call would mark this agent failed in exactly the months an alert fires, because 1 means a finding, not a failure. 0 and 1 are both success; only 2 — could not run — stops the script. VIGIE_DIGEST_DOW gates the Monday audience digest on the same agent rather than a second plist.

gate-watch — here a cron failure would be worse than a visible error: unauthenticated gh search prs returns zero rows, byte-identical to "no repo has any PRs", so it would report full coverage forever. It checks gh auth status explicitly rather than inferring from an empty result set. Monthly because a dormant repo starting to take PRs is a slow signal.

db-backup — pg_dump of every database in ~/.claude/db-backup.json, to iCloud outside dev-mirror. LaunchAgent because the connection strings are in the login keychain. The plist PATH carries /opt/homebrew/opt/libpq/bin because libpq is keg-only. It needs the DIRECT Neon host: a pooled (-pooler) string is rewritten, but store the direct one. A missing keychain item is exit 2 (unknown), and an empty or tiny dump is exit 1, never a success. Retention and the size floor live in the script.

umami-retention — deletes Umami visit data older than 13 months (owner decision 2026-10-19), monthly on day 2. LaunchAgent because the DIRECT Neon URL (item umami-retention) is in the login keychain; plist PATH carries libpq like db-backup. One all-or-nothing transaction; sessions are removed only when no event, session_data or revenue row still references them. The script owns the number and the table list. A connected-but-not-Umami database is exit 2, never "nothing to delete". Take a Neon snapshot before the first real deletion.

elenchus-watch — the hour is the design, not a free slot. The proxy's counters roll over at 00:00 UTC, computed inside the Durable Object from its own clock. 23:47 Paris is 21:47 UTC — ~91% of the UTC day elapsed in summer. A morning run would sample a UTC day a few hours old and read near-zero every single day, reporting "plenty left" on the very day the service refused everyone at 23:00. Do not move it to a breakfast slot to sit beside the others. The route it reads is deliberately invisible rather than closed: an unauthorized caller gets the same 405 Method not allowed as any other GET, byte-identical, because a 404 or 401 would announce it exists. Its secret is not the extension's X-Elenchus-Key, which ships inside the .crx. A 405 is exit 2, not exit 0. Status reads claim no quota — a test pins that six status reads around one analysis move the counter by exactly one, so watching the service cannot consume what it watches.

netlify-plan-watch — the Netlify account is on the Legacy Free plan, which has no credits. On a credit plan each production deploy costs credits, and running out pauses every site on the account, the hub included. So the event worth catching is Netlify ending Legacy, and the job watches two sentences on the "How credits work" docs page: the Legacy one and the deploy-cost one. No deploy counter, deliberately: on Legacy a deploy costs nothing, so counting deploys measures nothing. A finding never rewrites the baseline. It re-alerts every week until the owner reads the page and runs the script with --accept; an alert that clears itself after one push is lost in a busy week. A missing page anchor is exit 2: the page moved, and nothing was measured. It reads no keychain, and it is a LaunchAgent only so that jobs-inventory.sh sees it.

distribution-watch — are the publish-once channels actually done: npm, PyPI, awesome-list PRs, the Chrome Web Store listing. Reports state, never proposes content, and checks for an already-open PR before naming "open a PR" as an action. The 3rd and 17th rather than the 1st and 15th, which are taken at 08:07.

dev-snapshot (~/Library/LaunchAgents/com.example.dev-snapshot.plist, 03:17) — one-way mirror of ~/Dev onto iCloud Drive, rsync -a --delete. A git remote covers tracked files only; this covers gitignored personal files, repos never pushed anywhere, and model weights that live in a project but never went to GitHub. Mirror, not archive: a local deletion propagates within the next run (worst case ~24 h), and the only undo is iCloud's own 30-day Recently Deleted — this script has no undo of its own. 03:17 was picked clear of every other agent, the 05:xx model jobs in particular; jobs-inventory.sh gives the occupied slots before any move. Two guards stand between a partially-wiped ~/Dev and a wiped mirror. A git-count floor refuses to run at all (exit 2) when ~/Dev holds fewer */.git dirs than a measured floor — read as UNKNOWN, never as "nothing to mirror". And because --max-delete only caps a real rsync's deletions (openrsync deletes up to the limit, exits 25, and would do the same again every subsequent night — a slow-motion wipe on the same ~30-day timescale as the iCloud undo window), the script runs a read-only pre-flight first and refuses the entire run (exit 1, nothing touched) when the pre-flight would delete at or above that same threshold. --max-delete stays on the real transfer too, as a redundant second fence. Deletions under */.git/ are not counted: a git gc repack deletes thousands of loose objects at once, and counting them refused the mirror every night for a week in October 2026. Do not "tighten" the guard back. A third guard refuses (exit 2) if the source and the iCloud destination are nested inside each other. See ~/.claude/scripts/dev-snapshot.sh for the filter list and every constant's rationale — each script owns its own numbers, not this file. The plist starts it through icloud-host.py (the iCloud listing rule above): launched directly, the pre-flight exited 23 every night from 2026-10-15 to 2026-11-07 while manual runs from a session succeeded. A clean run used to be silent, which broke the rule that log age is the only stop signal — a dead agent never exits with an error, it goes quiet, so only the age of its log can say it has stopped. launchd does not touch a log file's mtime on a run that writes nothing, so a job that ran fine every night for a month and one silently un-triggered for a month (laptop asleep at 03:17, agent unloaded) looked identical — an old, empty log and a cached last_exit 0 from months back. Fixed: every run, success included, now ends with one timestamped summary line (<ISO8601> dev-snapshot ok: <N> files, <D> deleted, <dur>s, exit 0, or FAILED: exit <rc> on stderr), so jobs-inventory.sh's log-age column is a real liveness signal for this job too, not an exception to the rule. Every non-zero exit also notifies once through notifier.sh (job name, exit code, counts — never a path). The liveness line cannot carry a failure on its own: vigie drops exit 1 on the assumption that the job already reported its finding, and the FAILED line keeps the log fresh, so a mirror refusing every night read as healthy everywhere. An evicted mirror copy is unlinked, not read. With the disk near full, iCloud evicts mirror files to dataless placeholders; when the source copy then changes, rsync mmaps the placeholder as its delta basis, a LaunchAgent may not fault it back in, and the whole transfer dies on mmap: Resource deadlock avoided (EDEADLK) — every night from 2026-10-16. --whole-file does not help under openrsync. The script unlinks the dataless copies the pre-flight says it will overwrite; any other read error still fails the run. To reproduce outside launchd, run rsync with the process's dataless materialisation policy set to off (setiopolicy_np) — from a terminal the file is silently downloaded and the bug hides. Its push has a cooldown per cause: the same failure re-pushes only after the script's cooldown, a new cause pushes at once, and the first clean run after a failure pushes one "recovered". A --dry-run stays outside it: it never pushes "recovered" and never reads or writes the cooldown state. The log keeps a line every night.

config-backup (~/Library/LaunchAgents/com.example.config-backup.plist, 03:47) — commits and pushes the two private config repos, ~/.claude and ~/.claude/plugins/local; what ~/.claude tracks is decided by its own .gitignore allow-list, never here. LaunchAgent because git push authenticates through the login keychain. A secret in the index is exit 2 with nothing committed — gitleaks git --staged runs before the commit and the index is reset, so every following night stops at the same place until a human looks; the repos' pre-commit hook is a second scan, not the gate. It also sweeps in whatever was left uncommitted in ~/.claude that day, by design: it is a backup, not a history. 03:47 is after dev-snapshot and well before the 05:xx model jobs. Ends every run with one summary line, for the same log-age reason as dev-snapshot.

midas-ohlcv-bridge (retired 2026-09-04) — the last of the quota-outage bridges, and the only scheduled job that ever wrote to a remote. It outlived the reset on purpose: while my-trading-app main carried a required status check, hosted fetch-ohlcv could not push and this bridge, under the owner's credentials, was the only OHLCV writer. It was retired only after the check was removed and a hosted run had pushed once. Do not rebuild it — my-trading-app is public and unmetered, so a hosted job is strictly better than one that needs this Mac awake.

Third-party agents

Non-com.example.* plists: ls ~/Library/LaunchAgents/ | grep -v '^com.example.'. Most only watch or update themselves. Pearcleaner's homebrew-autoupdate is the only scheduled job on this machine that changes installed software unattended — brew update && brew upgrade --greedy && …, and --greedy upgrades even casks that manage their own updates. Kept as-is by decision, recorded because an agent that writes and is not written down is how a version change becomes an unexplained breakage a week later — which is exactly how the fnm/node switch surfaced.

~/.claude/scripts/ — the conventions

Deterministic work gets a script, not an agent. The live roster comes from ls ~/.claude/scripts/; never write it down.

Each script is the source of truth for its own numbers. disk-hygiene.sh owns every retention number, tech-debt-triage.sh owns the four signals and their weights, env-drift-check.py owns the two documentation tiers, claude-md-weight.sh owns the instruction-file size threshold. The command files describe them and must never re-implement or restate them — restated numbers drift.

Targets are DISCOVERED, never hand-listed. gate-watch.sh from one gh search prs --owner call, usage-watch.sh from the Vercel API, model-watch.sh from each repo's wrangler.toml, claude-md-weight.sh by walking ~/Dev. This is not decorative: a hand-picked loop over 12 repos missed my-boardgame-app and my-bias-app, both of which had PRs.

Watchers report state; they never edit config, raise a ceiling, or open a PR. Choosing a replacement model needs an eval set (elenchus/scripts/bake-off.mjs); raising an Elenchus ceiling needs the provider's published RPD re-derived in the same change. gate-watch.sh distinguishes pending (a fix already open in a PR) from MISSING, because a watcher that nags about work in flight is one you learn to skip.

notifier.sh is the single push channel, and its topic is PUBLIC. It redacts against secrets.json literals first and generic token patterns second, because a failing run can quote its own curl command. That redaction covers secrets, not personal data — never route personal content through it. Counts only. A transport failure is retried, then spooled — never dropped. A DNS hiccup used to lose the push outright (one attempt, exit 1). The notifier now retries transient failures only (transport, HTTP 5xx, 429; any other 4xx is an answer), then writes the message to a bounded spool that the next call reaching ntfy sends first, flagged with its queue time; overflow is counted and pushed, not discarded. Exit 1 is unchanged. A deferred (--a) message is never spooled: its caller re-arms it itself, so a spooled copy would arrive twice. The script owns the delays and the bound.

Anything that rotates or rewrites a job log must preserve its mtime. The log's age is the only signal that an agent has stopped, and jobs-inventory.sh reads it straight from the mtime — a rotation that bumps it makes every rotated job read as having run today, the silent kind of wrong. disk-hygiene.sh's job-log tier takes a touch -r reference before it truncates, rewrites the file in place (a running job holds it open for append; a mv would send its output to an unlinked inode) and restores the mtime from the reference; its test proves an unguarded truncation reads 0 days. That script owns the line counts. Logs no com.example.* plist writes are listed and never touched — daemon.log belongs to the Claude Code daemon.

env-drift-check.py: tier 1 is .env.example/.dev.vars.example, tier 2 is README.md/CLAUDE.md prose. Conflating them reports every prose-documented secret as a leak-grade finding — the exact false alarm it was built to stop. It exits 1 only on a var documented in neither.

distribution-watch.sh's Chrome Web Store check asserts the resolved slug, never the status code — the Store answers 200 for any 32-character ID, including one that never existed, so res.ok is unfalsifiable there. It re-proves that control at runtime rather than trusting the measurement in its own comment.

All but distribution-watch.sh honour CLAUDE_DIR / DEV_DIR / LAUNCH_AGENTS_DIR so they can be tested against a fixture tree. That one reads no local tree — it queries public registries, so there is nothing to redirect.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.