Files
scripts/CLAUDE.md
Lucas BergerandClaude Opus 5 dad8774ed9 Document verified odin access path and Unraid API
Replace the assumed/unconfirmed environment notes with facts verified from
this box, and add docs/odin-access.md as the access reference.

Verified:
- Tailscale is the only path from dev to odin. dev (100.94.16.46) is a VPS
  with a public IP, not a LAN machine; odin is 100.101.253.105, and a subnet
  route puts 192.168.90.0/24 over tailscale0 at the same ~34ms RTT.
- The myunraid.net host is the canonical nginx vhost, not a cloud relay --
  DNS resolves it to the private 192.168.90.103, so traffic stays on the
  tailnet. The bare IP 404s on /graphql, and plain HTTP 302-redirects to the
  myunraid host while stripping the x-api-key header.
- GraphQL is live: unauthenticated POSTs return HTTP 200 with an
  UNAUTHENTICATED error body, so 200 must never be read as success.

Corrects unraid-docker-manager's claim that no SSL ignore is needed: TLS
verification fails from dev because ca-certificates 20250419 cannot chain
Let's Encrypt intermediate YR1. The cert is genuine and the claim likely
holds on odin itself, but curl needs -k here.

Container-polling field behaviours (UPPERCASE state, /-prefixed names,
PrefixedID ids, no isUpdateAvailable in 7.2) are carried over from
unraid-docker-manager, which is credited as the authoritative source.

No API key is present on this box; .env.unraid-api is gitignored there and
absent from the clone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-15 20:33:29 -04:00

9.9 KiB

scripts

Operational scripts for Luc's self-hosted environment. Each script collection is an independent, self-documenting folder at the repo root. There is no shared runtime, no build step, and no top-level package manifest — this is a repo of standalone tools, not an application.


The one hard rule: collections are self-contained

Every script collection lives in its own top-level folder and carries all of its own documentation, configuration, and dependencies. A collection must be understandable, runnable, and deletable on its own.

scripts/
├── CLAUDE.md              # this file — repo-wide context
├── README.md              # human-facing index of collections
├── _template/             # scaffold for a new collection (leading _ = not a collection)
└── <collection>/
    ├── README.md          # REQUIRED — what it does, prerequisites, usage
    ├── CLAUDE.md          # OPTIONAL — agent context, only if non-obvious
    ├── .env.example       # REQUIRED if the collection reads secrets
    ├── bin/               # executable entrypoints (chmod +x, shebang)
    ├── lib/               # sourced helpers, not directly executable
    └── docs/              # anything longer than the README warrants

Do not create shared/, common/, or utils/ at the repo root. If two collections need the same helper, copy it. Duplication across collections is explicitly preferred over coupling — a collection that depends on a sibling is no longer independently deletable, and these scripts get deployed to different places at different times.

Folders prefixed with _ are repo infrastructure, not collections.

Adding a collection

Copy _template/ to the new name, fill in its README, and add one line to the root README.md index. That is the whole process.


The environment

odin — the Unraid server; the single host everything self-hosted runs on. Reached from this box over Tailscale (100.101.253.105, LAN 192.168.90.103).

See docs/odin-access.md for the access path, the WebGUI/API endpoint, TLS caveats, and the Unraid GraphQL API — all verified from this box.

Verified characteristics:

Aspect Detail
Host OS Unraid
Container runtime Docker + Docker Compose (stacks defined per-app)
Database MariaDB 11.x — PostgreSQL is not available, treat as a hard constraint
Cache / queue Redis (available, used optionally)
Auth Authelia — OIDC/OAuth2 provider for internal apps
Ingress Pangolin/Newt tunnel — no open inbound ports
DNS Split-DNS on bergerhouse.net; private IPs resolve internally
Git forge Self-hosted Gitea/Forgejo at git.bergerhouse.net (user luckberg, SSH port 222)
CI Gitea Actions with a self-hosted runner
Mail / calendar Fastmail (paid) — JMAP/CalDAV, the source of truth for calendars
Remote access Tailscale; odin 100.101.253.105, subnet route for 192.168.90.0/24
Unraid API GraphQL at {myunraid.net host}:8443/graphql, x-api-key auth (Unraid 7.2+)
Automation n8n on odin — drives the Docker-manager Telegram bot

Known service hostnames: git.bergerhouse.net, familysync.bergerhouse.net, familysync-dev.bergerhouse.net.

Not on odin: GitHub (gh is not installed — use tea for the Gitea forge), PostgreSQL, any cloud provider.

This box vs. odin

The machine this repo is edited on is dev (Tailscale 100.94.16.46) — a separate workstation, and in fact a VPS with a public IP, not a machine on the home LAN. Scripts written here usually target odin. Never assume a script runs on the same host it was authored on: take the target as a parameter or read it from config.

Tailscale is the only path from dev to odin — if it is down, nothing below resolves.

? — still unconfirmed: the SSH login to odin (no ~/.ssh/config entry on this box) and where deployed scripts are expected to live on the Unraid host.

Sibling repos on git.bergerhouse.net

luckberg/ holds scripts (this repo), familysync, unraid-docker-manager, azure-epac, _cargo-index. Two are load-bearing context:

  • unraid-docker-manager — n8n + Telegram bot managing odin's containers. The authoritative source on the Unraid GraphQL API and container polling; distilled into docs/odin-access.md.
  • familysync — the origin of most of the stack facts in the table above.

Prior context: MemPalace

Much of what is known about this environment lives in MemPalace, a local semantic memory store — not in this repo. Query it before asking Luc to re-explain something, and before assuming an infrastructure detail is undocumented.

Aspect Detail
Version mempalace v3.4.1, installed as a uv tool
Palace path /home/luc/.mempalace/palace
Backend ChromaDB (vector search) + SQLite (metadata); fully local, no API key
Access MCP server (mempalace-mcp) → 19 mempalace_* tools; plus the mempalace CLI
Size ~32k drawers

Read the docs, don't guess the CLI. MemPalace ships its own instructions:

mempalace instructions <help|init|mine|search|status>

Structure is Wings → Rooms → Closets → Drawers — wings are projects/people, rooms are topics, closets are summaries, drawers are verbatim memories. Halls connect rooms within a wing; tunnels connect rooms across wings.

Existing wings: familysync (~21.6k), sessions (~10.6k — mined agent transcripts), wing_familysync.

This repo's wing

Initialized with mempalace init . --yes and mined with mempalace mine ..

  • Wing: odin-scripts — set explicitly in mempalace.yaml, not auto-detected. Auto-detection uses the directory basename, and a wing called scripts would collide with any other corpus in a folder of that name.
  • Rooms: environment, documentation, collections, general.
  • Room routing matches keywords against the file path, so never use scripts as a room keyword here — it matches this repo's own path and swallows every file. (Learned the hard way; the comment in mempalace.yaml says so too.)
  • mempalace.yaml and entities.json are gitignored — that is MemPalace's own convention (issue #185), applied by init.

Re-mine after adding a collection: mempalace mine . --agent claude. Mining is idempotent — already-filed files are skipped.

Practical notes:

  • mempalace_search is semantic — the query field takes keywords only, max 250 chars. Put background in context, not in the query, or the embedding gets diluted.
  • Filter with wing/room when you know where the answer lives; the sessions wing is raw transcript chunks and is noisy for factual lookups.
  • BM25 scores of 0 with mid-range similarity means no lexical match — a bare-keyword search for a proper noun that returns only semantic neighbours is a negative result, not a weak positive. This is how the odin gap above was established.
  • mempalace_kg_query hits the temporal knowledge graph for point-in-time facts; prefer it over search for "what is X currently" questions, and use kg_supersede rather than invalidate-then-add when a single-valued fact changes.
  • Auto-save hooks are active: a Stop hook saves every 15 human messages, and a PreCompact hook force-saves before context compaction.

Complementary and separate from MemPalace:

  • Per-project agent memory~/.claude/projects/<slug>/memory/*.md, indexed by that directory's MEMORY.md. This repo's is currently empty; FamilySync's holds the CI-gate, dev-stack, and Gitea-CI notes that much of this file's environment section derives from.
  • Project docs on disk~/projects/familysync/CLAUDE.md and docs/ARCHITECTURE.md are the authoritative written source for the odin stack.

Conventions

These apply to every collection unless its own README documents a deliberate exception.

Shell

  • #!/usr/bin/env bash and set -euo pipefail at the top of every bash entrypoint.
  • Quote every expansion. Prefer [[ ]] over [ ].
  • Scripts must be idempotent — safe to re-run. Assume cron or a retry will do so.
  • Accept --dry-run for anything that mutates state, deletes, or sends. Default to the safe path when a flag is ambiguous.
  • Log to stdout, errors to stderr. No log files unless the collection documents rotation.

Secrets

  • Never commit secrets. Read them from the environment or a file path passed in.
  • Every collection that needs secrets ships a .env.example with dummy values and a README line naming where the real values live.
  • Note the FamilySync-side precedent: gitleaks runs as a blocking CI gate on that repo. Assume any secret committed here is treated the same way — as a leak to be rotated, not a mistake to be amended away.

Portability

  • Target the interpreters actually present: bash, Python 3, Node 22. Check before adding a dependency on anything else.
  • Unraid's userland is BusyBox-leaning in places — prefer POSIX-portable invocations of sed/awk/date over GNU-only flags when the script runs on the server.

Working agreements for Claude

  • Scope changes to one collection. A request about collection X should not touch collection Y. If it must, say so explicitly rather than doing it quietly.
  • Update the collection's README in the same change as the code. Docs living beside the script is the point of this repo's layout; a code change that leaves its README stale defeats it.
  • Don't invent infrastructure. If a script needs a host, path, port, or credential that isn't documented above, ask — do not guess a plausible value. Wrong infra assumptions in an ops script fail in production, not in review.
  • Destructive operations need confirmation before they're run against odin, even when the script itself is finished and correct. Writing the script is not authorization to execute it.