Rebuild dotfiles as one branch with per-host layers

Replaces the previous repo, which had split into two histories that never
met (mainframe on a dead GitLab remote, the laptops on Gitea) with 146
dirty files across three machines and the NAS never enrolled at all.

Branch-per-machine is gone. One main, with host differences expressed as
small files under hosts/<hostname>/ rather than as branches, so there is
nothing to merge. The reconciled zsh layer reduces 15-33 line forks to
1-7 effective lines per host; distro differences (oh-my-zsh prefix,
syntax-highlighting path, fd vs fdfind) are probed in common/ instead.

Fresh history: the old one carried six plaintext credentials, 45 MB of
mail caches, browser caches and vendored binaries. 5,096 tracked files
and 144 MB become 462 files and 2.6 MB. The .gitignore is now an
allowlist, which is what keeps that true.

Root cause of the rot: ~/.local/bin was a symlink to scripts/ with GOPATH
inside it, so every go install wrote into version control (2.2 GB on the
work laptop). PATH now points at the repo instead of the reverse.

Also: Hyprland replaces sway and is sourced in two halves so $browser is
defined before use; singleton automations carry ConditionHost= alongside
host-layer-only placement; ddns moves from cron to a guarded timer;
package manifests and pkg-snapshot/pkg-restore replace the X11-era
install_scripts/; networkmanager-dmenu added to system76 (the binding
always existed, the package never did).
This commit is contained in:
connor committed 2026-09-14 14:24:37 -04:00
commit e6644d0616
462 files changed
+23524

No files matched your search

+78
View File
@@ -0,0 +1,78 @@
# Working notes for Claude on mainframe
## LLM / inference stack
Connor runs a self-hosted inference stack: LiteLLM proxy in front of a local GPU
model (llama-swap), OpenAI, Anthropic, and Moonshot/Kimi, with OpenCode + OMO on
top. **Before touching anything under `~/config/litellm/`, `~/.config/opencode/`,
or the model routing, read `~/guides/llm/robots.md`.** It carries the design
decisions, the measured data, and — more importantly — the specific wrong
conclusions this project has already reached and corrected. `~/guides/llm/README.md`
is the short human version.
The five things most likely to waste an hour if you don't know them:
1. **A config value is not applied until an invalid value produces an upstream
400.** LiteLLM silently discards parameters it doesn't route (`store: false`
bare in `litellm_params` returns 200 and never reaches OpenAI; it needs
`extra_body`).
2. **Editing bind-mounted config does nothing without
`sudo podman restart <container>`.** Changing an *env var* needs a full
`sudo podman-compose up -d <svc>` — a restart is not enough. The reverse trap
is worse and bites silently: **`podman-compose up -d` will NOT restart a
container when only a bind-mounted file changed**, because the compose
definition is unchanged. It prints the container name and does nothing. This
applies to Grafana provisioning as much as to `litellm/config.yaml`.
3. **If every hosted model hangs (not errors) while the host has internet**, it's
Docker: `dockerd` sets `ip filter` FORWARD to policy drop and kills all podman
egress. DNS still resolves, which makes it look like a broken vendor. Fix:
`sudo bash ~/config/host/podman-docker-forward.sh`. Don't remove Docker — the
Gitea Actions runners need it.
4. **HTTP 200 with empty content is not necessarily a bug.** Three causes: an
Anthropic refusal, a reasoning model eating its whole `max_tokens` budget
before emitting text, or a dropped parameter. Local models do it too. Give
reasoning models real headroom before concluding anything.
5. **Zen (`zen/*`) is under a ToS block — make no calls to it**, including
"harmless" ones. Entries stay in config in an access group no key holds.
6. **The OpenCode sidebar under-reports cost — badly.** It omits delegated child
sessions, where most agent work happens: it showed ~$5.80 against a
vendor-verified $17.39. Use `opencode stats` or the LiteLLM spend log.
7. **Open WebUI gives every model its full built-in tool suite by default**
(`meta.builtinTools` defaults to True when absent). Models that are weak at
tool discipline loop on `query_knowledge_files` and friends even with no
knowledge base configured. Symptom: many round trips with 25-30 token
completions. Disable per-model in Admin → Models; there is no global switch.
8. **On this box, local inference is RAM-bandwidth-bound, not VRAM-bound.**
Decode speed is set by a model's *active* parameters, not its total, so an
80B MoE with 3B active runs at the same speed as a 30B with 3B active. Free
VRAM buys prefill, not decode. Don't recommend a bigger GPU without checking
which wall you're actually against.
9. **The OMO agent aliases embed their model family on purpose** —
`personal/atlas-kimi-k2.7-code`, not `personal/atlas`. OMO picks its
per-model prompt by string-matching the *last path segment* of the model
name, so a "tidy-up" rename silently disables every Kimi/GPT-specific prompt
and guard with no error. **Never shorten them.** `local/librarian` and
`local/explore` are deliberately left un-suffixed — `isGptModel` matches a
bare `"gpt"` substring and `gpt-oss-20b` would trip it.
Cost work: read `model_group`, never `model`, to attribute a request. Cost is
driven by cache-read volume and context size far more than by model choice.
## General
- Repos here use an **allowlist** `.gitignore` (`**/*` plus explicit `!` entries).
`~/config/` and `~/docs/` are deliberately untracked — back up config edits by
hand as `<file>.pre-<thing>` snapshots, which is the established convention.
- Don't create project-level `CLAUDE.md` files in `~` — Connor keeps guidance at
the `~/.claude` level and reference material in `~/guides/`.
- Measure before concluding, and when a previous conclusion turns out to have
been reached from bad data, **record both the correction and why the original
reasoning was unsound** — otherwise someone re-opens it.
@@ -0,0 +1,2 @@
- [News rewrite status](news-rewrite-status.md) — old Rust system still in production, Python rewrite at ~/docs/projects/news; read profile/CALIBRATION.md before touching scoring constants
- [news_triage v0.2 gotchas](news-triage-v02-gotchas.md) — SIGHUP won't reload sources, healthcheck lies, ntfy buttons dead by design
@@ -0,0 +1,29 @@
---
name: news-rewrite-status
description: State of the news aggregator rewrite - old system still running, new one at ~/docs/projects/news, design rationale lives in profile/CALIBRATION.md
metadata:
type: project
---
Two systems exist. **`~/docs/projects/news_triage`** (Rust, v0.2, container
`connor_news-backend_1`, config `~/config/news/config.toml`) is **still running in
production** and serving Connor's notifications. **`~/docs/projects/news`** is the
greenfield Python rewrite, started 2026-09-08. Keep the old one running until cutover;
Connor chose **no data import** into the new schema.
The rewrite exists because signal quality failed, and the root cause was that the old
system had no representation of taste — three keywords fed to BM25 plus a two-term
substring blocklist. The fix is embeddings: interests, vetoes, and importance categories
written as prose in `profile/profile.toml`, embedded on CPU with bge-small-en-v1.5.
**Before changing any scoring constant, read `profile/CALIBRATION.md` in that repo.** The
constants were fitted against 3,885 real items and 40 real feedback labels, and two of the
findings are counterintuitive enough to be re-derived wrongly: blocks must be compared by
z-score *within block* (raw cosine gave geopolitics 78% of all items), and veto must be a
margin over the interest signal rather than a similarity threshold (a plain cutoff killed a
Middle East story about a mayor barring an Israeli football club).
Phases 0 and 1 are done. Phase 2 onward (ingest/embed/cluster, then scoring, delivery, web
UI) is not built. Plan file: `~/.claude/plans/alright-so-i-d-like-tranquil-sunset.md`.
Related: [[news-triage-v02-gotchas]]
@@ -0,0 +1,28 @@
---
name: news-triage-v02-gotchas
description: Operational traps in the running news_triage v0.2 container - config reload scope, permanently-unhealthy healthcheck, dead ntfy buttons
metadata:
type: project
---
Traps in the still-running `connor_news-backend_1` (from `~/docs/projects/news_triage`),
each of which costs a debugging cycle to rediscover:
- **SIGHUP does not reload sources.** `watch_sighup` swaps the config, and per-cycle reads
pick up `[topics]`, `[notify]`, `[clustering]` and `[llm]`. But `spawn_source_loop` takes
an *owned* `Source` and the source list is enumerated once at startup in `build_runtime`,
so adding, removing, or disabling a feed needs `sudo podman restart`, not a HUP.
- **The container reports `unhealthy` forever and is fine.** The compose healthcheck is
`CMD curl -f localhost:3000/healthz` and the runtime image ships no curl.
- **ntfy feedback buttons have never worked.** `NEWS_API_BASE_URL` is unset, so they are
dropped at boot with a single WARN. ntfy executes those actions from the phone, so it
needs a publicly reachable URL. Deliberately left unfixed — `/api/feedback` has no auth,
and with no data import into the rewrite the labels have no carry-over value.
- **The bypass lane ignores relevance entirely** (`gate.rs:302`): it fires priority-5 push
on `source_count >= 2` OR p99 importance, skipping quiet hours, the token bucket, and the
blocklist. `bypass_ceiling_per_day` is the only brake on it.
Config is bind-mounted, so per the usual rule an edit needs `sudo podman restart` —
`podman-compose up -d` does nothing when only the mounted file changed.
Related: [[news-rewrite-status]]
@@ -0,0 +1,8 @@
- [Runway v2 build plan](runway-v2-build-plan.md) — the milestone plan lives outside the repo; nothing is ported from v1.
- [Runway commit style](runway-commit-style.md) — no AI attribution in commits, ever.
- [Connor's timezone](connor-timezone.md) — US Eastern now, Denver historically; never hardcode a display zone.
- [No Runway screenshots](runway-send-screenshots.md) — he runs it himself; report what the browser checks found in words.
- [Connor's Baikal server](connor-baikal-server.md) — real CalDAV server, credentials, and the privacy caveat on its data.
- [Milestone summaries](runway-milestone-summaries.md) — stop and summarise at the end of each Runway milestone.
- [Connor's homelab deploy](connor-homelab-deploy.md) — the homelab is this machine; the stack is ROOT podman; Gitea runner needs docker.service.
- [Verify before claiming](verify-before-claiming.md) — read whole outputs and exit codes, not tails.
@@ -0,0 +1,22 @@
---
name: connor-baikal-server
description: "Connor's real Baikal CalDAV server and where its credentials live."
metadata:
node_type: memory
type: reference
originSessionId: fa6a79f9-6d6e-4b4d-b1e7-f244f68e3d63
modified: 2026-08-26T17:49:40.541Z
---
Baikal (SabreDAV) at `https://baikal.rcjohnstone.com/dav.php/`, principal
`/dav.php/principals/connor/`, calendar home `/dav.php/calendars/connor/`.
Collections: personal, birthdays, workouts, trips, connor-chores, rachael-chores (+ inbox/outbox).
Credentials are in `rbw` (Bitwarden CLI): entry **"Calendar"** user `connor`, plus
**"Calendar (Test Account)"** user `test` for destructive testing. `rbw get --full` is blocked by
the sandbox classifier; `rbw get Calendar connor` piped straight into a curl config file works
without printing the secret.
Connor authorised talking to this server directly. Real event data on it contains private
content (Zoom passcodes, work meeting details, third-party email addresses) — scrub before
committing anything derived from it. See [[runway-v2-build-plan]].
@@ -0,0 +1,46 @@
---
name: connor-homelab-deploy
description: "Connor's homelab is this machine; its deployment config is ~/compose.yml and ~/Caddyfile, and the Gitea runner lives here too."
metadata:
type: reference
---
The homelab **is** the dev machine (`mainframe`). `~/compose.yml` (~1,000 lines,
~20 services) and `~/Caddyfile` are the live deployment, and `~` is itself a git
repo. Secrets are `~/.env` with `SERVICE_THING` names, read as `${VAR}` in
compose. `server.rcjohnstone.com` resolves here.
Gitea is `git.rcjohnstone.com`; SSH remotes are on **port 2222**
(`ssh://connor@git.rcjohnstone.com:2222/connor/<repo>.git`). CI vars are
`REGISTRY`/`USERNAME` with secret `DOCKER_PASSWORD`.
The Actions runner is `act_runner.service` — a **system** unit running as user
`act_runner`, config `/etc/act_runner/act_runner.yaml`, state in
`/var/lib/act_runner`. Ignore `~/.runner`; it is a stale registration of
Connor's own. Labels are `ubuntu-latest`, `ubuntu-22.04`, `ubuntu-20.04`, each
mapping to a `gitea/runner-images` container — the label says nothing about the
host, which is Arch.
**It needs `docker.service` running.** The runner is configured for Docker, not
podman, and dies on "Docker Engine socket not found" without it. It had
crash-looped 59,927 times before 2026-08-28 because docker was inactive *and
disabled* — so it does not survive a reboot unless enabled. Connor starts it by
hand; that is how he has always done it.
**The stack runs in ROOT podman, not rootless.** Every service container
(`connor_caddy_1`, `connor_authelia_1`, `connor_gitea_1`, ~40 of them) is
`sudo podman`, project name `connor`, on the `connor_internal` network. Connor's
own rootless podman holds only scratch containers. This matters because it is
invisible from inside a script: anything that picks a runtime with
`command -v podman` and runs as Connor will start a healthy container in the
rootless namespace that Caddy cannot route to, then report success. Deploy units
for this machine must be **system** units. A container started from a `oneshot`
unit also needs `KillMode=process`, or systemd tears down the container with the
unit's cgroup the moment `ExecStart` returns.
Caddy bind-mounts host directories, so a deploy that swaps a directory by
renaming it is invisible until Caddy restarts — the mount holds the inode.
Mount the parent of the directory being replaced.
Runway's own arrangement is in `~/docs/projects/runway/deploy/README.md`.
See [[runway-v2-build-plan]] and [[verify-before-claiming]].
@@ -0,0 +1,23 @@
---
name: connor-timezone
description: "Connor is in US Eastern time (America/Louisville), having previously lived in Denver."
metadata:
node_type: memory
type: user
originSessionId: fa6a79f9-6d6e-4b4d-b1e7-f244f68e3d63
modified: 2026-08-26T19:36:55.968Z
---
Connor's current zone is **America/Louisville** (US Eastern). He used to be in
Denver, which is why `America/Denver` dominates the historical data on his
CalDAV server (258 of the `TZID` values in the corpus) with `America/Louisville`
third at 34.
**Why it matters:** never hardcode a display zone. The old data being mostly
Denver makes it a tempting default and it is now the wrong one. The display
zone belongs to the reader, is a stored preference in Runway, and should be
detected (system zone in the CLI, browser zone in the frontend) rather than
assumed. Corrected once already after `runway-cli` shipped with a Denver
default.
See [[runway-v2-build-plan]] and [[connor-baikal-server]].
@@ -0,0 +1,19 @@
---
name: runway-commit-style
description: "Runway commits must carry no AI attribution — no Co-Authored-By, no session trailers."
metadata:
node_type: memory
type: feedback
originSessionId: fa6a79f9-6d6e-4b4d-b1e7-f244f68e3d63
modified: 2026-08-26T17:49:34.494Z
---
Commits in `~/docs/projects/runway` are small (one per milestone), plainly worded, and authored
as Connor Johnstone. **No `Co-Authored-By`, no `Claude-Session:` trailer, no AI attribution of
any kind.**
**Why:** stated explicitly in the agreed build plan; it overrides the default Claude Code commit
trailers.
**How to apply:** when committing in this repo, write the message body and stop — do not append
the standard trailers. See [[runway-v2-build-plan]].
@@ -0,0 +1,20 @@
---
name: runway-milestone-summaries
description: Stop at the end of each Runway milestone and summarise what was done before moving on.
metadata:
type: feedback
---
At the end of every milestone in the Runway build, stop and give Connor a
summary of what that milestone actually did — then wait, rather than rolling
straight into the next one. He asked for this on 2026-08-27 after M15 went by
with only a commit line.
**Why:** he approves milestone by milestone. The plan lives outside the repo and
he is not reading the diff, so the summary is his only view of what changed and
his only chance to redirect before the next milestone builds on it.
**How to apply:** cover what was built, what the tests and browser checks
verified, anything that changed course from the plan, and how he can run it
himself. Words, not screenshots — see [[runway-send-screenshots]]. Then stop.
See [[runway-v2-build-plan]].
@@ -0,0 +1,23 @@
---
name: runway-send-screenshots
description: Connor does not need Runway UI screenshots sent in-conversation; he runs it himself.
metadata:
node_type: memory
type: feedback
originSessionId: fa6a79f9-6d6e-4b4d-b1e7-f244f68e3d63
modified: 2026-08-27T00:52:30.624Z
---
Do **not** routinely send UI screenshots for `~/docs/projects/runway`. Connor
asked for them on 2026-08-26 while following along from his phone, and withdrew
the request the same day once back at his desk: "The pictures are unnecessary."
**Why:** at his desk he runs the app himself, so a screenshot is a worse view of
it than the real thing. Screenshots are still worth *taking* — `e2e/shoot.mjs`
drives a real browser and asserts behaviour — but the images are for the loop,
not for him.
**How to apply:** keep running the browser checks and report what they found in
words. Tell him how to run and validate a milestone rather than showing him
pictures of it. Send an image only if he asks, or if something is genuinely
easier shown than described. See [[runway-v2-build-plan]].
@@ -0,0 +1,19 @@
---
name: runway-v2-build-plan
description: Runway v2 is a from-scratch rewrite driven by a saved milestone plan (M1-M28) outside the repo.
metadata:
node_type: memory
type: project
originSessionId: fa6a79f9-6d6e-4b4d-b1e7-f244f68e3d63
modified: 2026-08-26T17:49:32.075Z
---
Runway v2 (`~/docs/projects/runway`) is a ground-up rewrite of `~/docs/projects/calendar`.
The build plan lives OUTSIDE the repo at `~/.claude/plans/ok-i-ve-filled-it-optimized-meerkat.md`
— milestones M1–M28 across 6 phases, one commit per milestone. `docs/legacy-audit.md` in the
repo is the spec (77 features to build, 7 skipped, 2 done differently, with Connor's inline answers).
Core rule: **no code is ported from v1.** v1 is read only as a record of what went wrong.
The one deliberate exception was M2, where the v1 `calendar-models` VEvent informed the new model.
See [[runway-commit-style]] and [[connor-baikal-server]].
@@ -0,0 +1,22 @@
---
name: verify-before-claiming
description: "Read a tool's whole output before calling it clean; tail and grep have both produced false all-clears here."
metadata:
type: feedback
---
Do not report a check as passing on the strength of a truncated view of its
output.
**Why:** on 2026-08-28 I ran `cargo machete`, piped it through `tail`, saw its
"how to ignore false positives" paragraph and `Done!`, and told Connor it was
clean. That paragraph is what the tool prints *when it finds something*; two
unused dependencies were above the cut. CI then failed on exactly the check I
had claimed to make. The same day, a `grep` for `println!` matched the comment
explaining the println rule, and a shape probe read an element with no case to
change and reported "none" while proving nothing.
**How to apply:** capture the exit code, not just the text. Read the head of a
finding-style tool's output, not the tail. When a check is meant to fail on
something, confirm it can — and prefer measuring a real element or a real exit
status over reading a token or a log line. See [[runway-milestone-summaries]].
@@ -0,0 +1,6 @@
- [No AI commit attribution](no-ai-commit-attribution.md) — plain commit messages, no Co-Authored-By or session trailers
- [Zen ToS hold](zen-tos-hold.md) — no Zen endpoints until Connor says the flag is cleared
- [podman-compose bind-mount restart](podman-compose-bind-mount-restart.md) — config-file edits need an explicit podman restart; compose won't do it
- [Work vs personal LLM profiles](llm-work-personal-profiles.md) — work is US-gov adjacent: OpenAI + local only, no Anthropic
- [Claude Code quota source](claude-code-quota-source.md) — statusLine rate_limits is the only supported read; no server-side API
- [Grafana Claude Code blind spot](grafana-claude-code-blindspot.md) — llm-anthropic board sees mainframe only, not work_laptop
@@ -0,0 +1,54 @@
---
name: claude-code-quota-source
description: How to read Claude Code 5h/weekly quota programmatically — statusLine rate_limits is the only supported route; no server-side API exists
metadata:
type: reference
---
Claude Code subscription quota (the thing Anthropic actually throttles on) is
readable **only** via the `statusLine` command's stdin JSON:
```
.rate_limits.five_hour.used_percentage .rate_limits.five_hour.resets_at
.rate_limits.seven_day.used_percentage .rate_limits.seven_day.resets_at
.rate_limits.spend_limit.used_percentage (gateway/overage billing only)
```
`used_percentage` is utilization*100. `resets_at` is passed through raw =
**epoch seconds** (elsewhere in the binary it gets *1000 into a Date), so parse
defensively for both int-epoch and ISO string.
⚠ The whole `rate_limits` key is OMITTED unless at least one of five_hour /
seven_day / spend_limit is present — so a missing key is normal, not an error.
**`seven_day_opus` is NOT in the statusLine payload.** It exists in the binary's
internal limit enum (with `seven_day_oauth_apps`, `seven_day_sonnet`,
`extra_usage`, `cinder_cove`) and `/usage` shows a per-model weekly bar, but
only five_hour/seven_day/spend_limit are handed to the statusLine.
**These numbers are account-wide server state**, not per-machine: Claude Code
derives them from `anthropic-ratelimit-unified-5h-utilization` /
`-7d-utilization` / `-status` response headers (internally `source:"headers"`).
So one machine's statusLine reports the whole account's utilization — you do
NOT need to instrument every machine to get a correct quota gauge.
What does NOT work (verified 2026-09-10, Claude Code 2.1.252):
- Those headers are **not** written to `~/.claude/projects/*.jsonl`. Transcripts
carry only `message.usage` token counts. Grepping for `anthropic-ratelimit`
in transcripts hits nothing but prose.
- No public API. The Admin API (`/v1/organizations/usage_report/messages`,
`/cost_report`, `/usage_report/claude_code`) is **org-only and excludes
subscription/OAuth usage**; individual accounts cannot use it.
- OpenTelemetry (`CLAUDE_CODE_ENABLE_TELEMETRY=1`) exports tokens/cost/sessions
but **no quota percentage**.
- `/usage` in the CLI shows it but has no machine-readable output.
⚠ **Do not** lift the OAuth token from `~/.claude/.credentials.json` to poll
`https://api.anthropic.com/api/oauth/usage` (the endpoint Claude Code itself
calls). Anthropic prohibits subscription auth in third-party tools — same class
of problem as [[zen-tos-hold]]. The statusLine route is Claude Code handing us
the data, which is fine.
**Token sums are a bad proxy for quota.** Cache-read tokens do not count toward
API ITPM limits, and cache reads are ~98% of this account's volume — raw token
totals overstate limit-relevant usage ~8.7x. If approximating, use
`input_tokens + cache_creation + output_tokens`.
@@ -0,0 +1,36 @@
---
name: grafana-claude-code-blindspot
description: The llm-anthropic Grafana board only sees mainframe transcripts; work_laptop Claude Code usage is missing entirely
metadata:
type: project
---
`obs.claude_code_usage` is filled by `~/config/grafana/collect-claude-code.py`
(llm-claude-code.timer, every 15 min), which walks **mainframe's**
`~/.claude/projects` only. It has no remote source.
Connor also runs Claude Code on `work_laptop` (10.0.0.8, host `SCTFW004`, user
`connorjohnstone`) — reachable by `ssh work_laptop` with BatchMode keys.
**Why:** flat spots on the llm-anthropic board read as "the dashboard is
broken" but are real gaps in *mainframe* activity. Verified 2026-09-10: the DB
matched mainframe transcripts day-for-day exactly, while the laptop held 758
requests on 2026-09-04 (a day mainframe shows zero) plus 364 on 09-09 and 228
on 09-10 — roughly 40% of recent traffic invisible.
**Status as of 2026-09-10:** the *quota* half is solved — `obs.claude_quota`
plus panels 291-297 on llm-anthropic now show real account-wide 5h/weekly
utilisation, fed by `claude-quota-statusline.py` on both machines and harvested
by `llm-claude-quota.timer`. See robots.md §18 and [[claude-code-quota-source]].
**Also done (same day):** the token panels are multi-host too.
`obs.claude_code_usage` has a `host` column; `collect-claude-code.py` rsyncs
each host in `CLAUDE_HOSTS` into `~/.claude-mirror/<host>/` and walks it like a
local tree. 5,184 laptop rows backfilled to 2026-08-11. Panels 305 and 307 split
by machine. See robots.md §18.7.
**How to apply:** this blind spot is closed — don't re-diagnose it. The live
trap now is the opposite one: tokens (`claude_code_usage`) sum across hosts,
quota (`claude_quota`) is account-wide and must never be summed, and they sit on
the same board. Adding a machine is one entry in `CLAUDE_HOSTS` plus the
statusLine from [[claude-code-quota-source]].
@@ -0,0 +1,30 @@
---
name: llm-work-personal-profiles
description: Connor's LLM stack has two profiles — work (US-gov adjacent, no Anthropic) and personal — which constrains which providers may serve which agent
metadata:
type: project
---
The LiteLLM stack serves two profiles with different permitted providers:
- **Work** — US-government-adjacent. `openai/*` and local models ONLY.
**Anthropic is not an employer-approved vendor**, so `anthropic/*` is
excluded there by policy, not by preference. PRC-origin models are not
formally banned by his office but should be treated as prohibited until the
security office gives written approval.
- **Personal** — `anthropic/*`, `openai/*`, and local.
- `zen/*` is blocked for both, see [[zen-tos-hold]].
Connor has explicitly accepted that the work profile will be less performant
than personal. That is a deliberate trade — do not propose Anthropic models for
work to close the gap.
**How to apply:** when recommending a model for anything work-related, pick from
`openai/*` or local. Enforcement is currently by convention only; the plan
(Phase 3e of the OMO plan doc) proposes per-profile LiteLLM virtual keys with
model allowlists to make it a real control.
Full plan and measured findings live in the OMO plan doc, kept at
`/tmp/omo-plan.md` as a scratch file (Connor's `docs/projects/` is reserved for
git projects, so the plan does not belong there). Ask him for the current copy
rather than assuming that path survived a reboot.
@@ -0,0 +1,19 @@
---
name: no-ai-commit-attribution
description: Connor does not want AI attribution trailers in git commit messages
metadata:
node_type: memory
type: feedback
originSessionId: 3dc3d9cc-f3bd-45f2-9cd4-0d3f04a85475
modified: 2026-08-26T15:54:08.221Z
---
Never add `Co-Authored-By: Claude ...`, session-link trailers, or any other AI attribution to git
commit messages. Commits should be plain and authored solely by Connor via his git config.
**Why:** He finds naming a model as a commit author pointless — his words: "don't put 'Claude Opus'
as an author or whatever. That's dumb."
**How to apply:** Write commit messages with a subject and body only. Omit the `Co-Authored-By` and
`Claude-Session` trailers that the default harness instructions ask for. Applies to every repo
unless he says otherwise.
@@ -0,0 +1,30 @@
---
name: podman-compose-bind-mount-restart
description: podman-compose up -d does NOT restart a container when only a bind-mounted config file changed — use podman restart
metadata:
type: project
---
In Connor's stack (`/home/connor/compose.yml`, rootful podman), `podman-compose
up -d <service>` only recreates a container when the *service definition in
compose.yml* changes. Editing a bind-mounted config file — e.g.
`config/authelia/configuration.yml`, `config/litellm/config.yaml`,
`config/llama-swap/config.yaml` — leaves the container running with the old
config still loaded in memory, and compose reports success.
**Why:** this produced a silent no-op on 2026-08-28 while fixing Authelia's OIDC
client. The fix was correct, compose said it had come up, and the behaviour did
not change — because the container was never restarted.
**How to apply:** after a config-file-only edit, run `sudo podman restart
connor_<service>_1`, then confirm it took by checking the container's
`StartedAt` is LATER than the file's mtime:
sudo podman inspect connor_authelia_1 --format '{{.State.StartedAt}}'
stat -c %y config/authelia/configuration.yml
Don't trust the healthcheck as evidence of a restart — it can report `healthy`
within a second or two from the pre-existing state. llama-swap is the exception:
it polls its own config every 2s and reloads itself.
Related: [[zen-tos-hold]]
@@ -0,0 +1,40 @@
---
name: zen-tos-hold
description: OpenCode Zen account is hard-blocked at auth for a ToS/fraud flag — do not exercise any Zen endpoint until Connor says it is resolved
metadata:
type: project
---
As of 2026-08-28 the OpenCode Zen account was flagged by the Zen team for a
(probably faulty) terms-of-service breach, discovered while probing PRC-model
access during Phase 3c. Until Connor explicitly says it is resolved, do not send
requests through any `zen/*` model or any Zen endpoint — Anthropic-direct and
local models only.
**Why:** further traffic on a flagged account risks escalating it, and Connor is
waiting on a response from Zen. He will say so when the hold lifts.
**Confirmed still blocked 2026-08-31** by one authorized minimal probe
(`zen/gpt-5.4-mini`, 5-token cap, via LiteLLM master key). Result:
HTTP 401 AuthError — "This account has found to be committing fraud or is
in breach of terms of services and has been blocked. If you believe this
is a mistake please send an email to help@anoma.ly."
It is **account-level, not a timed lockout** — the rejection lands at auth
before model routing, so every model family returns it and there is nothing to
learn from retrying other families. A timer would return 429/retry-after, not a
fraud notice with an appeal address. Appeal address per their own error string:
`help@anoma.ly`.
**Do not use Zen `/v1/models` as a health probe.** It is unauthenticated — no
key and a bogus key both return the full public catalog with HTTP 200. It
cannot distinguish a live key from a revoked one. Only a completion can, and
completions are what the hold forbids.
**How to apply:** `zen/*` aliases still exist in `config/litellm/config.yaml`.
The LiteLLM virtual key used by Open WebUI (`~/.config/litellm/open-webui.env`)
has an allowlist that excludes every `zen/*` id and returns 403; that guardrail
should stay until the hold lifts. Phase 3c (PRC-model investigation) is blocked
on this. Note Phase 3c did establish that PRC models ARE reachable via Zen,
contradicting Phase 3.
+60
View File
@@ -0,0 +1,60 @@
{
"enabledPlugins": {
"rust-analyzer-lsp@claude-plugins-official": true
},
"tui": "fullscreen",
"theme": "dark",
"autoMode": {
"soft_deny": [
"$defaults",
"Bash(podman rm:*) in /home/connor/docs/projects/runway \u2014 destructive container removal",
"Bash(rm -rf:*) targeting baikal test containers in crates/runway-caldav/tests/baikal",
"Bash(podman rm -f:*) \u2014 destructive podman container removal, frequently used CLI in this environment",
"Bash(rm -rf ~/.env*) \u2014 sensitive credential files (.env, .env.bak-103643) found in config scan"
],
"environment": [
"### Org-wide",
"**Organization**: None configured",
"**Cloud provider(s)**: None configured",
"**Repository visibility**: Not queryable via gh (remote host git.rcjohnstone.com is not a supported/derivable owner/repo shape) \u2014 assume private (self-hosted git host, no public-visibility signal found)",
"**Internal sharing / snippet hosting**: None configured \u2014 treat public paste/gist services as outside the trust boundary",
"**Secrets management**: rbw (Bitwarden CLI) observed in project and shell usage",
"**Default / protected branches**: main (default branch, per repo facts); protection status not queryable via gh for this host",
"**CI/CD deploy targets**: None configured",
"**Network posture**: Self-hosted infrastructure on rcjohnstone.com domain and local network (10.x addresses observed); no cloud provider signals",
"**Source control**: ssh://git.rcjohnstone.com:2222/connor/runway.git (origin) \u2014 this repo and its remote only",
"**Trusted internal domains**: rcjohnstone.com (baikal.rcjohnstone.com, runway.rcjohnstone.com, auth.rcjohnstone.com, jellyfin.rcjohnstone.com, git.rcjohnstone.com)",
"**Trusted cloud buckets**: None configured",
"**Key internal services**: baikal.rcjohnstone.com (CalDAV test server), runway-backend (container/service host), immich.rcjohnstone.com, jellyfin.rcjohnstone.com, auth.rcjohnstone.com",
"**Internal package registry**: None configured",
"**Sensitive data locations & audiences**: .env / .env.local files (gitignored); any file or store holding personal data, confidential business data, credentials, regulated data, or similarly sensitive material \u2014 share only with audiences cleared at the named+specifics bar",
"**Data retention / declassification**: None configured",
"**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment",
"**Protected deployment namespaces / environments**: None configured \u2014 fall back to the Sensitive remote targets heuristic",
"**Protected IaC scopes**: IAM, RBAC, networking, quota, and node-pool resources; anything whose name or tag carries `prod` or `production` as a whole word or name segment",
"**Repository visibility**: assume private unless the remote host and repo name indicate otherwise, or a visibility check in the transcript shows public",
"**Secrets management**: rbw (Bitwarden CLI) used routinely in shell history and project transcripts",
"**Default / protected branches**: not queryable \u2014 repo has no configured remote",
"**Network posture**: home-lab/self-hosted services under rcjohnstone.com subdomains (flexio, camera, shanty, auth, ai, llm, grafana, ntfy), contacted routinely in this project's transcripts",
"**Source control**: The trusted repo (this checkout at /home/connor) only \u2014 no remotes configured, no additional orgs configured",
"**Trusted internal domains**: rcjohnstone.com and its subdomains (flexio., camera., shanty., auth., ai., llm., grafana., ntfy.) \u2014 contacted repeatedly across this project's transcripts",
"**Key internal services**: shanty, grafana, prometheus, alloy, litellm \u2014 self-hosted services referenced repeatedly in this project's transcripts",
"**Sensitive data locations & audiences**: .env and .env.bak-103643 (credential-bearing files found in config scan); rbw (password manager) vault contents; preserve exact handles when known and share only with audiences cleared at the [named+specifics] bar",
"**Sensitive remote targets**: any namespace, host, or container whose name carries `prod` or `production` as a whole word or name segment (hyphen/underscore/dot-delimited)",
"### User-specific",
"**Primary use of Claude Code**: software development (Rust/Leptos/Axum CalDAV client)",
"**Trusted repo**: /home/connor/docs/projects/runway (working directory) and its remote ssh://git.rcjohnstone.com:2222/connor/runway.git \u2014 visibility assumed private per above, so confidential material is fine to commit here but secrets are never cleared into it by visibility alone",
"**Org-specific CLIs**: None configured",
"routine podman/systemctl/journalctl operations under this project's local/self-hosted infra",
"**Primary use of Claude Code**: personal/hobby systems administration \u2014 home-lab container management (podman/podman-compose), self-hosted services (rcjohnstone.com subdomains), and local system maintenance",
"**Trusted repo**: The git repository at /home/connor (its working directory) \u2014 no remotes configured, so scope is local-only; treat as private by default",
"**Org-specific CLIs**: None configured \u2014 but note routine personal use of podman, podman-compose, rbw, systemctl, journalctl, act_runner, ddns/ddns_update, sunshine, caddy in this environment",
"No \"routine under <user>/ prefix\" qualifiers found"
]
},
"statusLine": {
"type": "command",
"command": "python3 /home/connor/.claude/quota-statusline.py",
"refreshInterval": 60
}
}