@@ -2,193 +2,123 @@
> **Overwrite ** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md ](hub/CHANGELOG.md ); the scripts history lives in [scripts/CHANGELOG.md ](scripts/CHANGELOG.md ).
## CC workflow migrated from the Windows workstation to DooPlex — 2026-07-19
## Tailscale on DooPlex + felhom-pve — make the N100 location-independent — 2026-07-19
Claude Code now runs natively on **DooPlex (192.168.0.180, Debian 13, `kisfenyo`) ** , working directly
in `/mnt/5_hdd/felhom.eu/git/<repo>` . Builds are local commands; felhom-pve is one `ssh` hop;
`kubectl` is local. The Windows workstation can stay powered off. Docs/skills adapted in all four
repos, non-versioned assets migrated, environment wired and verified end-to-end.
**Not a code change.** Network/ops on two production hosts (DooPlex, felhom-pve). Run supervised,
normal permission prompts, one host at a time, verify-after-every-mutation. Baseline `felhom.eu`
`a771cda` .
**Not a code change** — no Go source, no `build.sh` , no ArgoCD/GitOps rule was touched. Only *where
the commands are typed from*.
### 1. Part 0 audit (verbatim highlights) + dependency classification
---
```
# felhom-pve host — STATIC, not DHCP (contradicted the operator's expectation)
iface vmbr0 inet static / address 192.168.0.162/24 / gateway 192.168.0.1
resolv.conf: search local / nameserver 192.168.0.250 (pi-hole)
### 1. Part 0 inventory
# PBS is OFFSITE, not on 192.168.0.180 (contradicted the task premise)
storage.cfg: pbs felhom-pbs server 10.77.0.1 datastore felhom-offsite namespace demo-felhom
ip route get 10.77.0.1 -> dev wg-felhom src 10.77.0.2
wg-felhom endpoint = 167.233.158.164:443 (Hetzner, public); handshake fresh, 3.75 GiB sent
| Asset | Class | Disposition |
|---|---|---|
| Repo docs (`CLAUDE.md` , `CONTEXT.md` , `REUSE.md` , runbooks, `README.md` ) × 4 repos | versioned | adapted in place, pushed (§2) |
| ` felhom.eu/skills/*` (4 skills — build-deploy, ui-design, testing, app-catalog) | versioned | adapted; installed on DooPlex as symlinks |
| `felhom.eu/scripts/install_skills.py` | versioned | made cross-platform (POSIX symlink / Windows junction) |
| `E:\git\CLAUDE.md` (workspace root, in no repo) | portable | ported → `/mnt/5_hdd/felhom.eu/git/CLAUDE.md` **and ** versioned copy at `documentation/runbooks/workspace-CLAUDE.md` (verified byte-identical) |
| `~/.claude/settings.json` | portable | adapted → DooPlex (§5) |
| `E:\git\.claude\settings.local.json` (workspace-scoped) | portable | **folded into ** the user settings; every entry was a Windows `$SSH` /path one-off with no Linux meaning |
| **Project memory — 119 files, 672 KB ** (`~/.claude/projects/E--git/memory/` ) | portable | **migrated ** → `/mnt/5_hdd/felhom.eu/git/.claude-memory/` , surfaced via `~/.claude/projects/-mnt-5-hdd-felhom-eu-git/memory` symlink. * Not anticipated by the spec * — flagged per its "migrate by the same rules" clause |
| `~/.claude/skills/*` (Windows junctions) | versioned source | recreated on DooPlex as POSIX symlinks by the fixed installer |
| Per-repo `.claude/settings.local.json` | — | **none exist ** (checked all repos) |
| `~/.claude/CLAUDE.md` | — | **does not exist ** on the Windows side; nothing to merge |
| Session history, `~/.claude.json` , `history.jsonl` , `shell-snapshots/` , `file-history/` | not portable | path-keyed / machine-local — **not copied ** , accepted loss |
| Stray workspace files (`felhom-agent-0.2x.0.bin` × 4, `felhom-opsign.exe` , `longhorn-faulted-diagnosis-*.md` , `prompt_template.txt` ) | not portable | build leftovers/scratch — **not copied ** ; left on Windows |
| Non-felhom repos (`jarr` , `recipe-importer` , `revfulop-calendar` , `sparkyfitness-export` , `misc-scripts` ) | out of scope | untouched |
# guest 9201 — already DHCP; grep 192.168.0.x found the pinned control-plane endpoint
pct config 9201 net0: ...,ip=dhcp
9201:/etc/felhom-bootstrap/bootstrap.json "endpoint": "192.168.0.162:8443"
felhom-agent listen_addr = 192.168.0.162:8443 (LISTEN 192.168.0.162:8443 users:(("felhom-agent")))
### 2. Commits pus hed
# ot her
grep 192.168.0.x in /etc/pve /etc/cron /etc/systemd (excl .162): (none)
pve-firewall: disabled/running (no Tailscale allow-rule needed)
DooPlex ipv4 ip_forward = 1 (k3s), ipv6 = 0 ; both hosts Debian 13 trixie
```
Windows batch (the last Windows pushes):
| Repo | Commit | Content |
|---|---|---|
| felhom-controller | `ac7323dc9a` | docs: migrate workflow to DooPlex-local execution |
| felhom-agent | `d81fddc7b1` | docs: migrate workflow to DooPlex-local execution |
| felhom.eu | `ee48a48288` | docs: migrate workflow to DooPlex-local execution |
| felhom-controller | `29eda5d86e` | docs(runbook): e2e live-drive commands are DooPlex-local |
| felhom.eu | `29b2b8ad7c` | docs: DooPlex-local commands in PROMPT-TEMPLATE + manual-build runbook |
| app-catalog-felhom.eu | `34d50a33ac` | docs: migrate workflow to DooPlex-local execution |
Pushed **from DooPlex ** (the new-home proof):
| Repo | Commit | Content |
|---|---|---|
| felhom-controller | `2fcae041ae` | chore: mark `build-samba-image.sh` executable (mode-only) |
| felhom.eu | `a868e3585a` | docs: DooPlex is the canonical CC environment (CONTEXT.md) + iso scripts executable |
| felhom.eu | `fa8c0d5972` | docs(workspace): record the migrated project-memory location |
**Final HEADs, all four trees clean on DooPlex:** felhom-controller `29eda5d86e` , felhom-agent
`d81fddc7b1` , felhom.eu `29b2b8ad7c` , app-catalog-felhom.eu `34d50a33ac` .
### 3. Grep-hit counts (env-specific references in live docs)
Scope: `*.md` + `*.py` , excluding `CHANGELOG.md` , `REPORT*.md` , and the historical
`documentation/{audits,tests,pilot}/` (history is history).
| Repo | Before | After | Remaining — all intentional |
| Dep | Where | Address | Survives the move? |
|---|---|---|---|
| felhom-controller | 47 | 36 | `CLAUDE.md` 4 + `RUNBOOK-e2e-live-drive.md` 2 = the new "Legacy: Windows workstation" notes; `docs/vscode-ssh-fix.md` 27 = the legacy doc, now carrying a LEGACY banner; * * `TASK.md` 3 = see §6** |
| felhom-agent | 4 | 4 | `CLAUDE.md` 3 = new legacy note; `PLAN.md` 1 = hi storical plan note |
| felhom.eu | 27 | 20 | `CLAUDE.md` 3 + `skills/felhom-build-deploy` 2 + `workspace-CLAUDE.md` 8 = legacy sections; `CONTEXT.md` 2 = the migration note itself; `install_skills.py` 3 = the deliberate Windows branch; `architecture/04` 1 ( `sk-ssh-ed25519@openssh.com` ) + `break-glass.md` 1 ( `/run/sshd` ) = **protocol/package names, never environment ** |
| app-catalog-felhom.eu | 1 | 0 | — |
| PBS backup | host storage.cfg | `10.77.0.1` via `wg-felhom` →Hetzner:443 | **Yes ** — offsite tunnel, internet-only |
| Default gateway | host | `192.168.0.1` | Yes — DHCP-derived once ho st is DHCP |
| DNS (pi-hole) | host + guest | `192.168.0.250` | Yes for general DNS; `gitea.dooplex.hu` split-horizon won't resolve off-LAN → **finding ** |
| **Agent listen + guest endpoint ** | agent.json + guest bootstrap.json | * * `192.168.0.162:8443` ** | **NO ** — hard-pinned to host LAN IP; L2-bridged → subnet routes don't help → **finding, record-not-fix ** |
| `*.demo-felhom.eu` LAN forward | home pi-hole → box | inbound | LAN-local demo breaks off-LAN; Cloudflare Tunnel external access unaffected → note only |
**No unadapted live instruction remains.** The one deliberate exception is `felhom-controller/TASK.md`
(§6). Genuinely-platform mentions were left alone by design: `_linux.go` /`_other.go` dev stubs
(`REUSE.md` ), `\\FELHOM` shares in Windows Explorer (`controller/README.md` ), the `openssh-client`
package in the restic image, and the Windows-grep-multibyte rationale in the gate scripts.
### 2. Tailscale nodes
### 4. Verification (all from DooPlex, verbatim)
| Node | Tailnet IPv4 | Route advertised | Key expiry | How installed |
|---|---|---|---|---|
| `dooplex` | `100.107.87.53` | `192.168.0.0/24` (approved) | disabled | **pre-existing ** k3s pod `admin-system/tailscale` (hostNetwork, GitOps) — * not * installed by this task |
| `felhom-pve` | `100.70.170.35` | — | disabled (operator-confirmed) | host apt package (trixie repo), `--accept-dns=false` |
`tailscale status` from the N100: both nodes listed/online. resolv.conf on felhom-pve **byte-identical**
after `tailscale up` (`CorpDNS:false` ).
**Course correction:** the task assumed Tailscale had to be * installed * on DooPlex and that PBS lived
on `.180` . Both were wrong — DooPlex already ran Tailscale as a GitOps k3s pod (the operator was
right; my `which tailscale` /systemd probe was too narrow and missed it), and PBS is offsite. The
redundant host-level Tailscale I briefly installed on DooPlex was **fully purged ** (package + repo +
keyring + sysctl file), verified: k3s `tailscale0` and the `dooplex` node stayed up, `ip_forward`
still 1.
### 3. accept-routes spike (§3.1) — CONFIRMED degradation, rolled back
Enabling `--accept-routes` on felhom-pve * while on `192.168.0.0/24` * :
```
=== felhom-controller (controller/) === BUILD+VET OK
=== felhom-agent (root) === BUILD+VET OK
=== felhom.eu (hub/) === BUILD+VET OK
=== kubectl (local) ===
NAME READY STATUS RESTARTS AGE
contact-mailer-5bb869b85b-pqtjt 1/1 Running 0 20d
felhom-webpage-69cdbfc47-jm7lv 2/2 Running 2 (9d ago) 18d
filebrowser-59ff87cd88-662kz 1/1 Running 0 38d
hub-6d859c4dff-r5vrx 1/1 Running 0 14h
umami-7cd7f95cd8-89wrn 1/1 Running 1 (38d ago) 42d
umami-db-5fd98f59c5-xhwfr 1/1 Running 0 38d
=== pve hop ===
pve-manager/9.2.2/b9984c6d90a4bd80 (running kernel: 7.0.2-6-pve)
=== guest 9201 (through the pve hop) ===
gitea.dooplex.hu/admin/felhom-controller:0.147.3 Up 2 hours (healthy)
=== claude + skills ===
2.1.205 (Claude Code)
felhom-app-catalog felhom-build-deploy felhom-testing felhom-ui-design
=== tmux === tmux 3.5a
=== disk headroom ===
/dev/sda1 9.1T 2.1T 6.6T 24% /mnt/5_hdd
/dev/sdb1 445G 284G 139G 68% /
=== repo HEADs / cleanliness ===
felhom-controller 29eda5d86e dirty=[]
felhom-agent d81fddc7b1 dirty=[]
felhom.eu 29b2b8ad7c dirty=[]
app-catalog-felhom.eu 34d50a33ac dirty=[]
=== live workspace CLAUDE.md vs versioned copy === IDENTICAL
PRE : ip route get 192.168.0.180 -> dev vmbr0 src 192.168.0.162 ; ping .180 avg 0.3 ms
POST: ip route get 192.168.0.180 -> dev tailscale0 table 52 src 100.70.170.35 ; ping .180 avg 68 ms (max 136)
table 52 contains: 192.168.0.0/24 dev tailscale0 ; ip rule 5270: from all lookup 52 (outranks main)
-> local subnet hijacked onto the tunnel; inbound SSH went asymmetric and STALLED
PBS (10.77.0.1 via wg-felhom) UNAFFECTED
ROLLBACK (via the tailnet path 100.70.170.35, which was immune): tailscale set --accept-routes=false
-> route to .180 back on vmbr0 direct
```
`install_skills.py` was exercised on DooPlex twice: first run created 4 symlinks, second run reported
"already installed" for all 4 with the repo's `skills/*/SKILL.md` intact and the tree clean —
confirming idempotency **and ** that the rmtree hazard (§5) does not fire.
Verdict: keep `--accept-routes=false` at home; it is a **travel-only opt-in ** (safe on a foreign
subnet). Documented with the reasoning in `documentation/operations/tailscale.md` .
**Explicitly NOT validated:** a full image **build → push → deploy ** cycle from DooPlex. Nothing
here required a build, and a docs-only change must not push an image. The first real task run on
DooPlex should **treat its build step as a validation step — watch it, do not assume. ** The pieces it
depends on are individually proven (local Go toolchain, registry creds already on the host, `ssh
felhom-pve` → ` pct exec 9201`), but the composed path has not run end-to-end.
### 4. SSH chain over the tailnet (§3.2)
### 5. Notable fixes made along the way
`~/.ssh/config` on DooPlex: `Host felhom-pve` → `HostName 100.70.170.35` ; added `Host felhom-pve-lan`
→ `192.168.0.162` . Proof:
- * * `install_skills.py` had a latent data-loss bug on POSIX.** `remove_existing()` used
`os.rmdir()` , which fails on a symlink-to-directory and fell through to
`shutil.rmtree()` — and rmtree following the symlink would have **deleted the repo's own
`skills/` sources**. Now symlinks are unlinked explicitly before any rmdir/rmtree path is reached,
and existence checks use `os.path.lexists()` so a broken link is replaced rather than ignored.
(Windows never hit this: junctions satisfy `os.rmdir` .)
- * * `skipDangerousModePermissionPrompt: true` was deliberately DROPPED** from the DooPlex settings —
it directly contradicts the new production-host guardrail ("do not run CC with permission prompts
disabled on this host"). Everything else portable was kept: `model: opus[1m]` , `effortLevel: high` ,
`tui: fullscreen` , `agentPushNotifEnabled` , the three plugins.
- **Permission allowlist rebuilt ** (30 rules): Windows one-offs dropped (`$SSH …` , `cygpath` ,
`SCP=/c/Windows/…` , `/e/git/…` paths, `dir:*` , `Read(//c/Users/…)` ), Linux equivalents added
(`sudo kubectl:*` , `ssh felhom-pve:*` , `python3:*` , `go test:*` , `df:*` ). **Also dropped: ** the
recipe-site WebFetch domains (`streetkitchen.hu` , `sobors.hu` , `kiskegyed.hu` , `gastrohobbi.hu` ),
the jarr/pnpm entries, and assorted malformed rules (`Bash(1:*)` , `Bash(done)` , `Bash(settings")` )
— all belonged to non-felhom projects that are not migrating.
- * * `PROMPT-TEMPLATE.md` §13 contradicted the GitOps rule**: it instructed `kubectl set image` for
hub deploys, which the hub docs explicitly forbid (auto-sync is OFF; imperative changes are
reverted on the next sync). Corrected to the manifest-bump + deliberate-sync method while the
block was being de-SSH'd.
- **Demo PVE host key had changed ** on DooPlex (the known N100-reprovision gotcha). The new key was
**verified identical to the one the Windows workstation already trusted**
(`SHA256:bcSDbVzxoLQhOURUDzRGf0zJheU3wyXR4EmQ4NEPuTo` ) before being accepted — not blindly
trust-on-first-use. `ssh-keyscan` was avoided (it hangs, per prior findings).
```
ssh felhom-pve -> pve-manager/9.2.2 ; pct exec 9201 docker ps -> felhom-controller:0.148.0 Up (healthy)
tailscale ping 100.70.170.35 -> pong via 192.168.0.162:41641 in 1ms (DIRECT over LAN, not DERP)
ssh felhom-pve-lan -> OK (LAN fallback works)
```
### 6 . Pending — Viktor
### 5 . Part 4 — host static→DHCP: **DONE ** (operator at console, reservation set)
1. * * `claude` login (OAuth) — the only blocking step.** Everything else is ready. On DooPlex:
```
ssh kisfenyo@192 .168.0.180
tmux new -A -s cc
cd /mnt/5_hdd/felhom.eu/git && claude
```
(`~/.local/bin` was appended to `~/.bashrc` ; a fresh login shell picks it up.)
2. **Push token: NOT needed. ** DooPlex already had a working `credential.helper=store` +
`~/.git-credentials` ; four pushes from DooPlex succeeded during this task. Nothing to issue,
nothing written anywhere — the credential stays out-of-band in `~/.git-credentials` .
3. * * `felhom-controller/TASK.md` is a stale spec** — it targets **v0.28.0 ** and the retired
`/opt/docker` compose deploy mechanism, and per the artifact taxonomy a `TASK.md` at a repo root
reads as "implement this". It was left untouched deliberately (not mine to delete). Recommend
deleting or moving it to `documentation/` ; it is the last live-looking Windows-command block in
the four repos.
4. **Remote-access preflight ** , before relying on this from outside the LAN: verify the SSH path to
DooPlex from that location. Gitea's **web UI is geo-fenced (HU/DE/US) ** — CC on DooPlex is
unaffected (it pushes over the LAN), but browser access to Gitea from elsewhere is not.
5. **Windows side is untouched ** and remains a stale fallback. Nothing was deleted there.
Reservation MAC `68:1d:ef:5d:a6:64` →`192.168.0.162` set by operator first (so the LAN-pinned agent
keeps working at home). Applied detached (`systemd-run … ifreload -a` ); result:
### 7. Observations (out of scope)
```
ifreload rc=0 ; vmbr0 inet dhcp -> got 192.168.0.162 back (reservation)
default via 192.168.0.1 ; felhom-agent active, still bound 192.168.0.162:8443 (no restart)
guest 9201: 12 containers ; Tailscale still direct 1ms
```
- **R-43/R-44 landed mid-task. ** `felhom-controller` had ~721 lines of uncommitted, build-clean
offsite-reconstitute work when this task started. Rather than commit unreleased data-restore code
under a docs commit, it was surfaced and left alone; you shipped it as **v0.148.0 (`062357f` )**
during the run. The Part 3 gate then compiled that code on DooPlex — `go build ./... && go vet
./...` **clean ** — so v0.148.0 has a green toolchain gate on the new host as a side effect.
- **Windows `grep -i` combined with `-F -f <patternfile>` silently returns zero matches ** in this Git
Bash build — it produced a false "0 remaining references" during verification and was only caught
by cross-checking on Linux. This is the same false-negative class the repos already warn about for
multibyte emoji; the counts in §3 are all from Linux grep. One more reason the Python gates exist.
- **Four DooPlex script files were mode-only dirty ** (`100644 → 100755` ) — the exec bit set when they
were run locally, which Windows commits cannot carry. Committed as mode-only changes so the new
clean-tree gate starts from a genuinely clean state; expect the same for any script Windows add ed.
- **The skill listing says three skills; there are four. ** `felhom-app-catalog` was missing from the
workspace CLAUDE.md's Skills section — corrected in the ported file.
- `~/.claude/projects/` on DooPlex already contained a `-mnt-5-hdd-felhom-eu` entry, which is what
confirmed the path-key mangling scheme (`_` and `.` both → `-` ) and let the memory symlink be
placed on the correct key rather than guessed.
Exact revert staged on the box: `/root/interfaces.static-revert-20260719` . resolv.conf is now
DHCP-managed (`.250` + `.1` ) instead of the old static `search local` — expected (Tailscale didn't
touch it; accept-dns rule intact).
### 6. Off-LAN legs NOT validated at home → vacation-day checklist
Cannot be tested until the box is on a foreign LAN: PBS from a foreign subnet, DERP-relay fallback,
off-LAN `gitea.dooplex.hu` resolution. The step-by-step is in `documentation/operations/tailscale.md`
(§ * Vacation-day checklist * ) — notably `ssh felhom-pve "pvesm status | grep -i pbs"` is the real
off-LAN PBS test.
### 7. Findings recorded, NOT fix ed
1. **Control plane pinned to `192.168.0.162` ** (HIGH for travel): `felhom-agent` `listen_addr` and
guest `bootstrap.json` `endpoint` both = `192.168.0.162:8443` . Off-LAN the agent can't bind →
agent down → controller can't reach it. Subnet routes don't help (L2 bridge). Needs a design
change (bind `0.0.0.0` + stable guest→host path). **The N100 is reachable and backs up off-LAN,
but its local operator plane is not yet portable.**
2. **Off-LAN gitea DNS ** : `gitea.dooplex.hu` (pi-hole split-horizon) won't resolve on a foreign LAN;
image pulls while travelling need the travel `--accept-routes` toggle + a temporary `/etc/hosts`
entry. Contingency ("Plan B") documented, not armed.
### 8. Commit
Docs commit: `__COMMIT__` (felhom.eu). Files: `documentation/operations/tailscale.md` (new),
`CLAUDE.md` + `skills/felhom-build-deploy/SKILL.md` env-table notes, `CONTEXT.md` decision entry,
this `REPORT.md` . No secrets committed (tunnel keys/tokens/PBS fingerprint referenced out-of-band).