This repo is public and meant to be forked. Not every app or config belongs
in its manifest: things that are personal (a specific streaming client), tied
to an employer, or just not worth another forker's review attention. extras
is the supported way to keep those out of the public tree without hiding them
from the machine that runs them.
A second, private repo — anywhere you like (a private GitHub repo, a private Gitea instance, a plain directory with its own git history). It is never referenced from this repo by path, URL, or submodule.
There are two separate mechanisms here, easy to conflate because they take the same path as input — set one and assume the other follows. It doesn't:
install.shrunning the scripts — the machine-level connection. One line in your own gitignoredlocal/secrets.env:EXTRAS_DIR="/home/you/code/thinkpad-extras"install.shsourceslocal/secrets.env, and ifEXTRAS_DIRis set to a real directory, it runs every*.shinEXTRAS_DIR/scripts/— flat, alphabetical — after the base layer and host profile. If a directory matching the detected host slug exists atEXTRAS_DIR/hosts/<slug>/(same slug this repo's ownhosts/<slug>/uses), its scripts run too, same base-then-host-profile order as this repo. That split only matters once the private repo runs on more than one machine — a single-machine extras repo can leavehosts/empty or absent. IfEXTRAS_DIRis unset,PLACEHOLDER, missing, orlocal/secrets.envitself doesn't exist yet, this is a silent no-op —install.shwarns and skips it, same as it does for every other authenticated step. Every fork behaves identically until its owner opts in. This file is off-limits to the agent — it holds real credentials, solocal/secrets.envis denied even for writes; you create and edit it yourself (cp local/secrets.env.example local/secrets.env).- The agent being able to read/write the private repo's files — a
session-level permission, unrelated to
install.sh. Add the private repo's absolute path toadditionalDirectoriesin your gitignored.claude/settings.local.json. Without this, the agent can still tell you what to run, but can't create or edit scripts/incidents there itself.
Neither one implies the other. It's entirely possible — and was, for a
while, on this project — to have agent write access to the private repo
while install.sh still silently skips it because local/secrets.env
hadn't been created yet.
Why not a submodule: a .gitmodules entry in a public repo names the private
remote's URL in plain text, and it breaks the clone for anyone forking this
who doesn't have access to that remote. A path read from a gitignored file
has neither problem.
- Create the private repo. Anywhere with git and, ideally, SSH access:
a private GitHub/GitLab repo, a self-hosted Gitea/Forgejo instance, even a
bare local repo with no remote yet. Nothing about
install.shcares where it lives. - Clone it somewhere under your own
~/code/(or wherever), sibling to this repo, not inside it. - Give it the same shape this repo uses, scaled down to what a private
layer actually needs:
The one thing intentionally still skipped: no
README.md incidents/ same convention as this repo's incidents/, own index.md incidents/_template.md docs/ procedure/narrative docs (not manifest companions) scripts/ PACKAGES.md narrated companion to the flatpak/package scripts, if any *.sh flat, idempotent, report-only — same contract as scripts/ health-checks/ optional — picked up by this repo's scripts/system-health-check.sh if EXTRAS_DIR is set, same exit-code contract as scripts/health-checks/ and hosts/<slug>/health-checks/. For personal app/workflow checks (a specific device's sync mount, a personal service) that don't belong in the public timer. hosts/<slug>/ only once this repo runs on more than one machine — same slug as this repo's own hosts/<slug>/. Most personal app/config choices travel with the person, not the hardware, so this stays empty until something genuinely differs machine-to-machine.local/in the private repo — secrets stay in this repo's gitignoredlocal/secrets.env, whichinstall.shalready sources before running extras' scripts. - Point this repo at it: add
EXTRAS_DIR="/abs/path/to/your-extras"to your gitignoredlocal/secrets.env(copy fromlocal/secrets.env.exampleif you haven't already). - If you want the agent to write there too — scripts, incidents, the
manifest — add the absolute path to
additionalDirectoriesin your gitignored.claude/settings.local.json. This takes effect on the next session start, not the running one (see CLAUDE.md's "Session shape"). - Run
./install.sh— it now runs the base layer, your host profile, then every*.shinEXTRAS_DIR/scripts/, then (if it exists)EXTRAS_DIR/hosts/<slug>/, in that order.
Default: a new app request goes to the private repo, not this one. This
repo's scope is the harness/infrastructure (scripts/, hosts/,
.claude/) plus the small foundational app set already in
scripts/install-flatpaks.sh — things bootstrap or recovery themselves lean
on, or that near enough any fork of this specific project would also want
(currently: an editor, a password manager). Everything else — a specific
app, a personal preference, anything employer-specific — defaults private.
Say "public" explicitly to override; the agent should ask rather than guess
on a genuinely unclear case, not default to the public repo because that's
the one it's sitting in.
Same contract run_script enforces on everything else in this repo:
- Idempotent — safe to run on every
install.shinvocation, changes nothing if the desired state already holds. - Executable (
chmod +x) orinstall.shrefuses to run it, loudly, rather than silently skipping a broken script. - Report-only where it's installing something reviewable — print what's
missing and the command to fix it, don't run the mutating command yourself.
See
scripts/install-flatpaks.shfor the pattern (anapp-id|reasonarray,flatpak infoto check, printedflatpak installlines to run by hand).
Copying that pattern means your private script reads and behaves exactly like the public ones, which matters more than it sounds: it's the difference between "check before acting" being a repo convention and being a rule you have to remember to re-apply for the one part of your setup nobody else can see.
Extras scripts run inside the same install.sh invocation, in the same
Claude Code session, under the same .claude/hooks/ guardrail layer as
everything else in this repo — the hooks watch the commands the agent
proposes and runs, not which repo a script came from. A private extras repo
is not a way to bypass CLAUDE.md's rules; it's a way to keep a fork's public
manifest relevant to the next forker.