infrastructure/skills/tailscale-up/SKILL.md
Tailscale runtime wiring for target:local hosts. Sets `--operator=$account` + `--hostname=$(hostname -s)` so user-systemd ExecStartPost can run `tailscale serve` without sudo, and the tailnet device name stays in sync with the system hostname. Depends on /ov-infrastructure:tailscale (which installs the daemon). Self-gates on `systemctl is-active tailscaled` so it's a no-op in image-build / pre-auth contexts. Use when adding deploy-runtime tailscale wiring to a target:local host (canonical consumer: local.ov-cachyos) — distinct from the tailscale layer which only installs + enables the daemon.
npx skillsauth add overthinkos/overthink-plugins tailscale-upInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
tailscaleThe tailscale layer installs the daemon and enables the systemd unit.
That's a build-time concern shared by bootc images, VMs, and any
other image that wants its own tailnet identity at boot.
The --operator and --hostname settings, however, are runtime
state that:
tailscale set has no socket to talk to).hostnamectl set-hostname new-name doesn't propagate to the tailnet device name automatically).Mixing those concerns into the shared tailscale layer would either
break bootc consumers (with build-time errors from missing daemon
socket) or require a target-aware conditional inside the shared layer
— exactly the kind of <name>-host polymorphism that CLAUDE.md's
"Init-system polymorphism" rule forbids.
tailscale-up is the runtime-config sibling: it depends on tailscale,
self-gates on systemctl is-active tailscaled, and only fires when
the daemon is actually up. Bootc consumers don't include it.
| Property | Value |
|----------|-------|
| Dependencies | tailscale |
| Packages | none (config-only layer) |
| Service | none (sets state on existing tailscaled.service) |
| Install files | task: (one cmd task) |
The layer's single cmd task:
tailscaled.service via systemctl start ... || true.
The /ov-infrastructure:tailscale layer is systemctl enable-only
(no --now) because the same task body runs at image-build time
where systemd isn't PID 1 and --now would fail; starting the
service is therefore a deploy-runtime concern that belongs in this
layer. Idempotent — no-op when already running, suppressed when
no live systemd is available.systemctl is-active --quiet tailscaled || exit 0
so the rest of the body is a no-op in any context where the daemon
isn't running (image build, masked unit, etc.).getent passwd 1000 | cut -d: -f1.
This matches the wheel-nopasswd layer and the
renderAurScript helper in ov/deploy_target_local.go — uid 1000
is the canonical "first human user" in this repo across CachyOS,
Arch, Fedora, Debian, and Ubuntu base images. SUDO_USER is
intentionally NOT used: runSudoShell calls sudo -n bash -s
without -E, so the default env_reset strips SUDO_USER on
strict sudoers configurations.tailscale set --operator="$account" (suppressed via || true
so a logged-out daemon doesn't fail the deploy)./etc/hostname directly (NOT hostname -s — the hostname
binary lives in Arch's inetutils package, which is NOT in the
minimal cloud-image base used by the eval-arch-vm R10 bed; reading the
file is binary-dependency-free and works on every distro). Calls
tailscale set --hostname=<short-form> (also suppressed) to keep
the tailnet device name in sync with the system hostname; the short
form is the value before the first dot, matching hostname -s
semantics.Both deploy-scope probes use a three-state pattern:
tailscale up performed).The "logged in" detection uses tailscale status --peers=false | head -1 | grep -q "Logged out" — false means logged-in.
tailscale-up-operator-readable: when logged in, asserts that
tailscale debug prefs succeeds AND emits a non-empty
"OperatorUser": line. Calling debug prefs without sudo is itself
the proof that --operator took effect (operator-or-root is the
authorization policy).
tailscale-up-hostname-tracks-system: when logged in, asserts that
the prefs Hostname field equals hostname -s.
local.ov-cachyos — primary consumer, applies it on every fresh
CachyOS dev workstation.target: local template that needs tailnet operator
permission.NOT used in:
target: pod deployment — pod tunnels go through the
tunnel: tailscale mechanism in deploy.yml (handled by the host's
tailscale, not in-pod). See /ov-core:deploy "Tailscale Serve"./ov-infrastructure:tailscale alone.tailscale set --hostname=<X> updates local prefs and is what the
layer task does on every deploy. For a fresh node that has not
yet run tailscale up, the prefs Hostname is what gets registered
with the control plane on first auth — so the tailnet device name
matches /etc/hostname automatically.
For a node that was already authed before this layer landed, the
device name on the tailnet is the value registered at original
tailscale up time and DOES NOT auto-rename when prefs.Hostname
changes. This is intentional Tailscale design — names are sticky
in the control plane to keep ACL rules, DNS caches, and audit trails
stable across reconfigurations. The local prefs are updated correctly
(visible via tailscale debug prefs | grep Hostname), but
tailscale status keeps showing the old name.
The layer surfaces this divergence as a hard-fail eval probe
(tailscale-up-device-name-matches-hostname). On every ov eval live <deploy> against a logged-in tailnet member, the probe compares the
short form of tailscale status --self --json | .DNSName against
/etc/hostname (cut to short form). If they differ, the probe fails
with stderr text identifying the registered device, the system
hostname, and both remediation paths. The probe self-gates on daemon
state (skips on a logged-out daemon) so test beds aren't affected.
Admin-console rename at https://login.tailscale.com/admin/machines — pick the device, click the kebab menu, "Edit machine name". Safe, no service interruption, no re-auth, preserves exit-node config and all other prefs. Recommended for any node with non-default flags.
sudo tailscale up --reset --hostname=$(cat /etc/hostname | cut -d. -f1) —
re-registers the device with the prefs Hostname.
WARNING: --reset clears every non-default flag (exit-node
config, advertised routes, accept-routes, accept-DNS-override,
advertise-tags, etc.). Only use this on a node where every flag is
at its Tailscale default.
The layer does not reach for tailscale up --reset automatically
because:
--reset to
fix a name divergence is a sledgehammer for a problem better solved
by an admin-console click or a deliberate operator-driven up.A proper auto-rename would require the Tailscale REST API
(PATCH https://api.tailscale.com/api/v2/device/{id}/name) plus an
operator-managed PAT or OAuth secret — significant credential-
management scope. If a future need arises for fully-automated rename
(fleet provisioning, CI runners), that's the right path; today's
manual remediation covers the actual single-host case.
# 1. Apply the ov-cachyos template — installs everything, including
# tailscale (daemon enabled) + tailscale-up (operator/hostname
# setters armed for next-time-up state changes).
ov deploy add ov-cachyos
# 2. Authenticate the daemon via the user's tailnet (browser SSO):
sudo tailscale up
# 3. Verify operator + hostname are wired correctly:
tailscale debug prefs | grep -E '"OperatorUser"|"Hostname"'
tailscale status | head -2
# 4. Optional re-apply (idempotent) to confirm the runtime task takes
# effect now that the daemon is logged in:
ov deploy add ov-cachyos
Subsequent ov deploy add ov-cachyos invocations re-run the runtime
task and re-confirm the operator + hostname state. Hostname changes
(sudo hostnamectl set-hostname new-name) propagate to the tailnet
on the next deploy.
MagicDNS is a tailnet-account-level toggle, not a per-node setting.
Enable it once at https://login.tailscale.com/admin/dns ; every node
logging in to the tailnet automatically gets 100.100.100.100 as a
DNS server (visible via resolvectl status | grep '100.100.100.100')
and the *.ts.net magic search domain.
tailscale-up does not touch DNS state — tailscale up (run manually
post-deploy) handles the per-node resolver wiring automatically on
systemd-resolved hosts (which CachyOS uses).
tailscale serve for pod deploys — direct, no restartThe --operator permission this layer wires is what makes the
direct, host-level tailscale serve workflow work without
sudo. For each pod that publishes a host port (e.g. immich-ml on
2283, ollama on 11434), one shell command on the host adds it to
the tailnet:
tailscale serve --bg --https=2283 http://127.0.0.1:2283
tailscale serve --bg --https=11434 http://127.0.0.1:11434
# ... one line per pod port
Each invocation writes to tailscaled.state (the daemon's local
BoltDB) and starts a listener on the tailscale interface that
forwards to the pre-existing host-loopback socket. Nothing about
the pod changes — no quadlet regen, no restart, no daemon-reload,
no port re-publish. The pod's published port on 127.0.0.1 was
always already there; tailscale serve simply adds a new path INTO
it from the tailnet.
To list current serves and their backends:
tailscale serve status
To remove a serve:
tailscale serve --https=2283 off
The serve config is persisted in the daemon's state and survives restarts, so a one-time setup is durable. Re-running with the same args is idempotent (replaces the existing entry verbatim).
tunnel: tailscale deploy.yml mechanism/ov-core:deploy documents a tunnel: tailscale field that emits
ExecStartPost=tailscale serve ... into the pod's quadlet. That
also works and is the right answer when (a) you want the serve
config tied to the pod's lifecycle (added on ov config, removed
on ov remove), or (b) you're willing to regenerate the quadlet
and restart the pod. For the more common "I have running pods, I
want them on the tailnet right now" case, the direct tailscale serve workflow above is faster, restartless, and doesn't touch
any source-tree state.
See /ov-core:deploy "Tailscale Serve" for the per-pod tunnel
matrix and /ov-automation:sidecar for the tailscale-sidecar pattern
(an alternative when a pod needs its own tailnet identity, not just
host-level serve).
/ov-infrastructure:tailscale — the install-side companion (daemon
install + systemctl enable). tailscale-up require: it./ov-core:deploy — deploy.yml tunnel: tailscale mechanism that
consumes the --operator permission./ov-local:local-deploy — the target: local execution model
this layer is designed for./ov-local:local-spec — local.yml template authoring; the
canonical consumer is local.ov-cachyos.wheel-nopasswd layer — provides the passwordless sudo
used by the eval probe's tailscale debug prefs chain./ov-image:layer — layer authoring reference./ov-eval:eval — declarative testing reference (three-state
pattern used here).Use when the user asks about:
tailscale serve fails with "permission denied" on a fresh
ov-cachyos host.hostname -s.development
Claude Code multi-agent support in Overthink — sub-agents, dynamic workflows, and agent teams, and how each drives the existing `ov eval` disposable beds to test and verify. MUST be invoked before authoring or invoking an ov sub-agent / dynamic workflow / agent team, wiring agent-lifecycle hooks, or asking "which primitive should drive the R10 beds?".
tools
Mounts a virtiofs share tagged `workspace` at /workspace inside a VM guest via a systemd .mount unit. Use when a kind:vm entity shares a host directory into the guest and you need it auto-mounted (and re-mounted at every boot).
development
MUST be invoked before any work involving: the `kind: android` schema kind, a `target: android` deploy, the `apk:` layer package format (installing Android apps declaratively), AndroidDeployTarget, an in-pod emulator OR a remote/physical adb-endpoint device, or nested `pod → android` deployment. The first-class Android device + app surface that sits above `ov eval adb`/`appium`.
tools
Use when committing, branching, pushing, merging, tagging, creating PRs, or approving/merging PRs with gh — the feat/-branch, R10-gated, never-force-push landing workflow across the main repo + the plugins submodule + image/<distro> submodules. Covers sync-to-upstream, branch/worktree pruning, the fork+PR path for contributors without write access, and cross-repo @github landing order.