agentleFS
novoadsnovoads/agent-skills/skills/novoads-update/SKILL.md

novoads-update

Skill28 starsChanged 3 months agoMITView on GitHub

Updates this skill pack from upstream, and owns the consent that decides whether it ever updates on its own. Checks whether the clone is behind, asks the user — update now, always keep me up to date, not now, or never ask again — and carries out the answer: applies the update through ./scripts/update.sh, records the consent flag the SessionStart hook already reads, snoozes on an escalating ladder, or turns the check off for good. Summarizes what changed between the two commits from the CHANGELOG. Use when the user asks to update or upgrade the pack, get the latest skills or the newest version, when the session banner reports that updates are available, when they ask whether their clone is current, when they want unattended updates switched on or off, or when they want to stop being asked. It never applies an update without an answer, and notify-only is the default.

  • Reads credentials
  • Deletes or force-pushes

The file

Show its rules and text

We split this skill into the rules it gives an agent

16 rules. About 0 are common, 9 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

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

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    If neither probe fires, stop here and offer none of the four options.

    line 65
  2. Distinctive:

    Run these from the repo root.

    line 80
  3. Distinctive:

    Read three things off that:

    line 91
  4. Distinctive:

    Use AskUserQuestion with exactly these four, worded as written.

    line 106
  5. Very specific:

    If that reports exit 127 / not found, you are not at the clone root — cd there and re-run.

    line 127
  6. Very specific:

    Never pass --fresh. The default target is the newest commit older than 24 hours, and that gap is the supply-chain buffer: it is the window …

    line 132
  7. Distinctive:

    Write the summary for someone who uses the skills, not someone who reads diffs: what is new, what changed under them, what broke and got …

    line 157
  8. Very specific:

    If the update went wrong, the way back is one command, and it is worth naming in the same breath as any conflict: ./scripts/update.sh …

    line 162
  9. Very specific:

    If update_check was off before this, tell the user you turned it back on and why: they had previously asked not to be reminded, and asking …

    line 188
  10. Distinctive:

    All three lines must print.

    line 210
  11. Very specific:

    If the grep finds nothing, say so plainly and stop short of promising anything: the flag is set but this clone's .claude/settings.json …

    line 249
  12. Very specific:

    Read the current level from the file if it exists.

    line 275
  13. Very specific:

    If auto_apply=on was already set, say that too.

    line 315
  14. Distinctive:

    Local edits to tracked files are stashed, never steamrolled: If the stash cannot be replayed cleanly, the work stays in the stash and the …

    line 336
  15. Distinctive:

    Do not batch the four options into a summary and pick for the user.

    line 350
  16. Distinctive:

    If the user asked to update and the answer is obviously option 1 from their own words ("update the pack"), still confirm the count and what …

    line 352

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “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

Update the pack

This pack is a git clone. Updating it is a merge, and a merge on someone else's working tree is a decision only they can make — so this skill's real product is the question, not the update. Ask, then do exactly what was answered.

Notify-only is the default and stays the default. Nothing here turns on unattended updating unless the user picks the option that says so, in their own words, in the dialog below.

Step 0 — confirm this is the pack clone, before anything else

This skill manages a cloned pack: a git repository with scripts/update.sh in it. It can also be installed on its own, copied into someone's ~/.claude/ alongside skills that have nothing to do with this repo — and in that shape every instruction below is pointed at the wrong repository. Step 1's git commands would report on the user's own project, and option 2 would write a consent flag into a directory no hook will ever read.

Every command in this skill runs from the clone root. Move there first, and keep doing it at the top of each block below — a session can open anywhere, and this is the one thing that silently changes what the commands mean:

root="$(git rev-parse --show-toplevel 2>/dev/null)" && [ -n "$root" ] && cd "$root"

if git remote get-url origin 2>/dev/null | grep -qE 'novoads/(claude-code-ads|agent-skills)' \
   || [ -f shared/scripts/auto-update.sh ]; then
  echo "pack clone confirmed"
else
  echo "NOT the pack clone — stop here"
fi

Both slugs, on purpose. The repo was novoads/claude-code-ads until 2026-08-13 and is novoads/agent-skills now. GitHub's 301 keeps every clone made before the rename working — git fetch follows it — but it does not rewrite origin in that clone's config, so those users' remote still reads the old slug and always will until they re-point it. Matching only the new name would tell every pre-rename clone it is not the pack, which is exactly the population this skill exists to update.

Git commands walk up to find the repository; plain paths do not. From a subdirectory ./scripts/update.sh is simply not found, [ -f shared/scripts/... ] reports a false negative on a real clone, and a .update-state/config written there lands somewhere no hook will ever read — consent recorded, nothing reading it, which is the exact failure this pack's design exists to prevent.

If neither probe fires, stop here and offer none of the four options. Say plainly:

This skill manages the cloned pack — the git repository with the skills and scripts/update.sh in it. This looks like a solo install, so there is no clone here to update. Solo-installed skills update with npx skills update. Be aware before running it: it replaces the installed files outright, with no backup, so copy any local edits somewhere safe first.

Then stop. Nothing here can update a solo install, and running the dialog would record consent that nothing reads — which is the exact failure this pack's design exists to make impossible. Do not offer to "try anyway".

Step 1 — find out where the clone actually stands

Run these from the repo root. None of them changes anything:

cd "$(git rev-parse --show-toplevel)"                 # every path below is root-relative
git rev-parse --abbrev-ref HEAD                       # expect: main
git fetch --quiet origin main 2>/dev/null || true     # bounded by git's own config
git rev-list --count HEAD..origin/main                # how many commits behind
git log --oneline HEAD..origin/main | head -10        # what they are
cat .update-state/config 2>/dev/null                  # current consent state

Read three things off that:

  • Behind count is 0 → say so in one line and stop. Do not run the dialog. A consent question with nothing to consent to is pure friction.
  • update_check=off in the config → the user previously chose "never ask again". They have now invoked this skill by hand, which is a fresh request and overrides the standing one, so continue. Mention that the check is off and offer to turn it back on. The switch silences the automatic surfaces; it was never meant to strand the manual path.
  • Not on main, or a detached HEAD → stop and say so. The clone is pinned to a commit on purpose. ./scripts/update.sh refuses this case too, and the fix is git checkout main by a human who knows why it was pinned.

Step 2 — ask, with these four options

Use AskUserQuestion with exactly these four, worded as written. The wording is the contract: each option is a different promise about what happens next, and a reworded option quietly changes which promise the user thinks they made.

OptionLabel
1Update now
2Always keep me up to date
3Not now
4Never ask again

Give the question a one-line header naming what is waiting: how many commits, and the headline of the newest one. Then act on the answer.

Option 1 — "Update now"

cd "$(git rev-parse --show-toplevel)"
./scripts/update.sh

If that reports exit 127 / not found, you are not at the clone root — cd there and re-run. Do not reach for git pull instead. It is not a workaround for this, it is the path that destroys a gitignored .env, and update.sh exists precisely because it does.

Never pass --fresh. The default target is the newest commit older than 24 hours, and that gap is the supply-chain buffer: it is the window in which a bad push upstream gets noticed by someone before it lands on a customer's disk.

The script prints one machine-readable STATUS= line. Branch on that line, not on any prose around it:

STATUSWhat to tell the user
updated FROM=<sha> TO=<sha>Updated. Then summarize, below.
currentAlready current. If stderr noted newer commits held by the cooldown, say that they land tomorrow.
updated_with_conflict FROM=… TO=… STASH=<ref>Updated, and their local edits are parked in <ref>. Lead with the stash — that is the part with their work in it. Recover with git stash pop <ref>.
offlineNo network. Nothing changed, the pack still works.
blocked REASON=<reason>Nothing changed and the repo is exactly as it was. Report the reason and its fix; do not try to work around it.
interruptedSomeone stopped the run (Ctrl-C, or the terminal went away). This is a safe state, not a broken one: any in-flight merge was aborted, .env was restored, and nothing is half-applied. The remedy is to run ./scripts/update.sh again — never a raw git pull to "finish the job", which is the exact thing this script exists to keep off the tree. Read its stderr before you report: if it stashed local edits first they are still in the stash and it says so, and in the one case it cannot clean up safely — a conflicted index with no stash behind it — it leaves the tree exactly as found and prints what to inspect. Exit code is nonzero.
rolled_back TO=<sha>Only from --rollback.

Then summarize what changed, in six bullets or fewer, from CHANGELOG.md between the two commits the STATUS line names:

git log --oneline <FROM>..<TO>
git diff <FROM>..<TO> -- CHANGELOG.md

Write the summary for someone who uses the skills, not someone who reads diffs: what is new, what changed under them, what broke and got fixed. Skip anything purely internal. If the CHANGELOG diff is empty, summarize the commit subjects instead and say that is what you used.

If the update went wrong, the way back is one command, and it is worth naming in the same breath as any conflict: ./scripts/update.sh --rollback.

Option 2 — "Always keep me up to date"

Three things happen, and all three are required. Writing the flag is the easy one; the other two are what make the flag mean anything.

a. Write auto_apply=on, and clear any standing update_check=off.

Granting "always keep me up to date" is revoking "never ask again". They are separate keys, and the hook checks the kill switch first and exits before it ever reads auto_apply — correctly, because off means off. So a user who once picked option 4 and now picks option 2 would otherwise end up with consent recorded, the reader registered, and nothing ever updating. Set both keys:

cd "$(git rev-parse --show-toplevel)"   # a config written elsewhere has no reader
mkdir -p .update-state
touch .update-state/config
# preserve every other key; replace or append the two this option owns
grep -vE '^[[:space:]]*(auto_apply|update_check)[[:space:]]*=' .update-state/config > .update-state/config.tmp 2>/dev/null || true
printf 'update_check=on\nauto_apply=on\n' >> .update-state/config.tmp
mv .update-state/config.tmp .update-state/config

If update_check was off before this, tell the user you turned it back on and why: they had previously asked not to be reminded, and asking for automatic updates supersedes that. Silently reversing an earlier decision is the kind of thing that has to be said out loud, even when it is obviously what they meant.

b. Verify BOTH halves, and say so out loud. The flag and the reader are different facts, and either one alone is a promise that will not be kept:

Assert each key is on, matching the exact set of values the hook accepts — on ON On true TRUE True yes YES 1. Do not test for the literal string off: the hook treats anything outside that on-list as off, so update_check=OFF, disabled or 0 would sail past an off-grep while the hook vetoes on them, and the skill would report all-clear on a clone that will never update. Check for the value that permits, not for one spelling of the value that forbids.

ON='(on|ON|On|true|TRUE|True|yes|YES|1)'
grep -q 'auto-update' .claude/settings.json && echo "reader: registered"
grep -qE "^[[:space:]]*auto_apply[[:space:]]*=[[:space:]]*$ON([[:space:]#].*)?\$"   .update-state/config && echo "flag: auto_apply is on"
grep -qE "^[[:space:]]*update_check[[:space:]]*=[[:space:]]*$ON([[:space:]#].*)?\$" .update-state/config && echo "kill switch: on, updates permitted"

All three lines must print. A missing line is a reason to stop and say what is wrong, not to round up to "should be fine".

Report the result in plain words: the flag is on, the kill switch is not standing in its way, and the SessionStart hook that reads it is registered in .claude/settings.json — so it takes effect at the next session start. Only claim that when all three checks agree.

c. Check for the machine-wide veto.

Mirror the hook's pass-through set here too, in the other direction: it ignores 0 false FALSE no NO off OFF and an empty value, and vetoes on anything else. Warning on any non-empty value would tell someone with NOVOADS_PACK_NO_UPDATE_CHECK=0 that their updates are blocked when they are not — under-promising is friendlier than the reverse, but it is still wrong.

case "${NOVOADS_PACK_NO_UPDATE_CHECK:-}" in
  ''|0|false|FALSE|no|NO|off|OFF) echo "env veto: not active" ;;
  *) echo "env veto ACTIVE: NOVOADS_PACK_NO_UPDATE_CHECK=$NOVOADS_PACK_NO_UPDATE_CHECK" ;;
esac

NOVOADS_PACK_NO_UPDATE_CHECK=1 in the environment overrides the config file and silences the hook no matter what was just written. If it is set, say so: the setting is recorded and will take effect once that variable is unset, and until then nothing will apply. Name it; do not fight it. Never unset it from here — it lives in the user's shell profile or their environment for a reason this repo cannot see, and a skill that quietly clears a machine-wide switch is worse than one that reports it.

This verification is not ceremony. The design it learned from shipped this exact dialog option, wrote this exact flag, and installed its reader only under a separate setup mode nobody ran — so a real install sat with consent switched on and nothing reading it for four months, reporting itself up to date while falling behind. In this pack the reader ships registered for every install and the flag only enables it, which makes that failure structurally impossible. The grep proves the property rather than assuming it.

If the grep finds nothing, say so plainly and stop short of promising anything: the flag is set but this clone's .claude/settings.json predates the hook. One manual update (option 1) brings the file in, and unattended updates begin after that. A promise the machine cannot keep is worse than no promise.

Then tell them what they just agreed to, in one sentence: at session start, at most once an hour, the pack will apply updates older than 24 hours by itself, and any session where it does will say so. And name the way out — this skill again, or auto_apply=off in the same file.

Option 3 — "Not now"

Snooze the banner nag on an escalating ladder. Write .update-state/update-snoozed:

level=<1|2|3>
until=<epoch seconds>
version=<sha of origin/main right now>
Existing levelNew levelQuiet for
none124 hours
1248 hours
2 or more37 days

Read the current level from the file if it exists; treat an unreadable or unparsable file as no snooze at all and start again at level 1. Get the version with git rev-parse origin/main.

Three rules govern it, and all three resolve toward speaking up rather than staying quiet:

  • A new upstream commit resets the snooze. The answer was about the version in hand, not about updates in general.
  • A file that does not parse is not a snooze. Fail toward notifying: a corrupt byte must never buy silence nobody asked for.
  • It suppresses the nag, and nothing else. ./scripts/update.sh and this skill both keep working while a snooze is live. A switch that strands the manual path is not a switch.

Confirm in one line: quiet until when, and that they can update any time by asking.

Option 4 — "Never ask again"

Set update_check=off in .update-state/config, using the same preserve-other- keys approach as option 2:

cd "$(git rev-parse --show-toplevel)"   # a config written elsewhere has no reader
mkdir -p .update-state
touch .update-state/config
grep -v '^[[:space:]]*update_check[[:space:]]*=' .update-state/config > .update-state/config.tmp 2>/dev/null || true
printf 'update_check=off\n' >> .update-state/config.tmp
mv .update-state/config.tmp .update-state/config

Say precisely what went quiet and what did not. Off silences the session-start check, the banner, and the auto-apply hook — no fetch, no network call from any hook. It does not disable ./scripts/update.sh, and it does not disable this skill. Updating stays available on request, forever; only the asking stops.

Name the way back in the same breath: update_check=on in that file, or invoking this skill.

If auto_apply=on was already set, say that too. This option does not erase it — the kill switch simply wins over it, which is why everything goes quiet — but it stays recorded, so turning the check back on later resumes unattended updates. A user who wants both off should be told to set auto_apply=off as well. Better to say it now than to have automatic updates reappear later as a surprise.

NOVOADS_PACK_NO_UPDATE_CHECK=1 in the environment does the same thing without editing a file, and it wins over whatever the config says. Mention it when someone wants the switch for one session, or for a whole machine, rather than for this clone.

What survives an update

Worth stating when the user hesitates, because the hesitation is usually about this:

  • .env is protected explicitly. It is copied aside before any merge and restored after, including on interrupt. This is deliberate and measured: a plain git pull silently destroys a gitignored .env when the upstream starts tracking that path.
  • Local edits to tracked files are stashed, never steamrolled. If the stash cannot be replayed cleanly, the work stays in the stash and the STATUS line names it. Conflict markers are never left in the working tree.
  • Untracked files are left alone unless an incoming file collides with one, in which case it goes into the same stash.
  • Customizations belong in local-skills/, which is untracked on purpose. Anything in skills/ is upstream's to overwrite.

Notes

  • This skill never runs git pull, git reset --hard, or git checkout on the user's tree. ./scripts/update.sh is the one writer, it is the only thing that has the .env protection and the stash policy, and going around it means going around both.
  • Do not batch the four options into a summary and pick for the user. The question is the product.
  • If the user asked to update and the answer is obviously option 1 from their own words ("update the pack"), still confirm the count and what is coming before applying — but do not re-ask the four-way question they already answered.

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.