This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. It is loaded automatically at the start of every session — treat everything here as standing rules that apply without being asked.
xzopia-deploy is the infrastructure automation CLI and documentation repo for the Xzopia managed IT platform. It covers six production servers, each with a defined role:
| Server | IP | Role | OS | Spec | SSH port |
|---|---|---|---|---|---|
| Server 1 | 213.171.194.170 | TacticalRMM Core | Ubuntu 22.04 | VPS 4-16-240 (4 vCore / 16 GB / 240 GB NVMe) | 2222 |
| Server 2 | 109.228.38.65 | Business Layer (ITFlow, Vaultwarden, Wiki.js, BookStack, SecureAgent) | Ubuntu 24.04 | VPS 4-16-240 (4 vCore / 16 GB / 240 GB NVMe) | 2222 |
| Server 3 | 79.99.41.247 | LIVE Comet Server (Xzopia BaaS) — cometd 26.7.0 licensed, backup.xzopiasecure.com (Caddy); client-revenue infra, backup-first | Ubuntu 24.04 | VPS 6-8-240 (6 vCore / 8 GB / 240 GB NVMe) | 2222 |
| Server 4 | 77.68.112.217 | MCP LIVE PRODUCTION — 12 connectors, mcp.xzopiasecure.com (nginx 1.31.1) | Ubuntu 24.04 | VPS 2-4-120 (2 vCore / 4 GB / 120 GB NVMe) | 2222 |
| Server 5 | 77.68.112.222 | FusionPBX VoIP | Debian 12 | VPS 2-4-120 (2 vCore / 4 GB / 120 GB NVMe) | 2222 |
| Server 6 | 77.68.127.221 | Wazuh SIEM candidate (Nextcloud build pending — XS item 54) | Ubuntu 24.04 | VPS 8-16-480 (8 vCore / 16 GB / 480 GB NVMe) | 2222 |
| Server 7 | n/a (LOCAL) | DR Drill Scratch VM — safe container host for the quarterly DR restore drill | Ubuntu 24.04 | LOCAL WSL2 distro xzopia-s7-drill — no fixed per-distro spec: shares the Windows host's allocation (reports 32 cores / 47 GB RAM / 1 TB root, verified 25 Jul 2026). NOT Fasthosts, not always-on, reached via wsl -d xzopia-s7-drill (no SSH) |
n/a |
Specs verified live 22 Jul 2026 (
nproc/free/lsblkper host — XS items 53/55). S2, S3 and S6 had been silently upgraded from their original plans: S2 2-4-120 → 4-16-240, S3 2-8-120 → 6-8-240, S6 2-4-120 → 8-16-480. S1 (4-16-240), S4 (2-4-120) and S5 (2-4-120, Debian 12) re-confirmed unchanged. S6's role line updates to its Nextcloud+Wazuh dual role once XS item 54 lands.
S7's spec line was wrong until 25 Jul 2026 — it recorded a "1 vCPU / 2 GB / 20 GB" allocation that does not exist. A WSL2 distro gets no per-distro cap: it shares the Windows host's allocation (WSL2 defaults to half the host's RAM), so S7 reports 32 cores / 47 GB / 1 TB. Consequence for testing: S7 is fine for functional checks, but it cannot produce a realistic per-instance resource footprint — anything sized on S7 numbers will be wildly optimistic. Capacity/sizing work needs a
.wslconfigcap or a real VM.
SSH key for all servers: ~/.ssh/xzopia_server1
Deploy user: xzopia (passwordless sudo)
Base domain: xzopiasecure.com
Server 7 is not part of the Fasthosts fleet. It is a LOCAL WSL2 VM on Simon's workstation, used only as the safe container-spin-up host for the quarterly DR restore drill (S4 has no Docker; S2 is live production). The SSH key / deploy user / base domain above apply to S1–S6 only. S7 has no routable IP and no SSH — it is reached via
wsl -d xzopia-s7-drill, kept powered off between drills, and does not go through the deploy pipeline or the fleet skills (audit / backup / pentest / pre-flight). Build + drill procedure:docs/runbooks/disaster-recovery.mdandscripts/server7/build-s7-drill.sh.
Naming convention (locked):
S1-EDR= SentinelOne (the EDR product / console / agents). BareS1= Server 1 only. UseS1-EDRfor the EDR product in all new writing; older pages may still use bare "S1" for SentinelOne — read those by context.
The repo contains:
src/) that SSHs into servers and runs setup scriptsscripts/) kept in sync with the TypeScript source.claude/skills/) for deployment, auditing, and securityconfig/servers.json) as single source of truthnpm on this workstation is hardened with ignore-scripts=true. Claude Code's real
CLI is a platform-native optional dependency (@anthropic-ai/claude-code-<platform>)
whose binary is linked into place by the package's postinstall (install.cjs).
The hardening blocks that postinstall, so the binary never links and Bash tool
calls intermittently fail with claude native binary not installed mid-session
(root cause + history: xzopia-state item 6, 22 Jul 2026).
Launch with the wrapper (self-heals the binary, then starts CC):
~/start-cc.sh # copy or symlink scripts/start-cc.sh -> ~/start-cc.sh
Manual repair (if ever needed, e.g. after a claude-code upgrade re-triggers it) —
a scoped reinstall; global ignore-scripts=true stays intact:
npm install -g @anthropic-ai/claude-code --ignore-scripts=false --include=optional --foreground-scripts
node "$(npm root -g)/@anthropic-ai/claude-code/install.cjs" # links the binary
claude --version # verify
Note: npm approve-scripts (npm 11's per-package allowlist) is project-scoped only
and does not apply to global installs — there is no persistent per-package
ignore-scripts=false for a -g install, which is why the wrapper / scoped-reinstall
approach is used. Re-run the repair after any claude-code global upgrade.
All CLI commands run from /home/simonkillen/xzopia-deploy via npm run dev -- <command>.
# Full setup on fresh Ubuntu 22.04 (all steps including SSH hardening)
npm run dev -- setup-server1-full \
--email simon@xzopia.com \
-i ~/.ssh/xzopia_server1
# Install TacticalRMM only (server already hardened)
npm run dev -- server1 install-tacticalrmm \
--email simon@xzopia.com \
--root-domain xzopiasecure.com \
-i ~/.ssh/xzopia_server1 \
--user xzopia \
--port 2222
# Full setup on fresh Ubuntu 24.04
npm run dev -- setup-server2-full \
--email simon@xzopia.com \
--base-domain xzopiasecure.com \
-i ~/.ssh/xzopia_server1 \
--port 22 # or 2222 if already hardened
# Update docker-compose.yml and restart all services (live change)
npm run dev -- update-server2-compose -i ~/.ssh/xzopia_server1
Passwords are saved to ~/.xzopia-deploy/server2-passwords.env (mode 600, never committed).
S3 is now the LIVE Comet Backup Server (cometd 26.7.0, licensed) at backup.xzopiasecure.com (Caddy) — client-revenue backup infrastructure, per config/servers.json (comet-baas). Manual changes only; snapshot/backup-first before any change. The former "decommissioning old-MCP" role is obsolete (the MCP stack long since migrated to S4). Live findings + full white-label branding capability: cc-report id=145. A deliberate Comet branding pass is pending Simon's go — do not apply branding without it.
⚠️ S4 is LIVE (12 MCP connectors, mcp.xzopiasecure.com). Never run setup-server4-full against the running box — the command below is kept for rebuild reference only. Snapshot / /backup-server --server server4 first for ANY change; after any change, verify all 12 connectors (scripts/server4/mcp-healthcheck.sh or curl each /mcp). The 12th is Companies House (mcp-companieshouse, port 3012, added this session — commit 9994d48).
# REBUILD REFERENCE ONLY — do NOT run against the live box.
# Full infrastructure setup (Nginx, Node.js 20, PM2, ClamAV, fail2ban)
npm run dev -- setup-server4-full \
--email simon@xzopia.com \
--base-domain xzopiasecure.com \
-i ~/.ssh/xzopia_server1 \
--port 2222
# Health check — services, ports, SSL, containers, ssh.socket
/audit-server --server server2
# Security scan — nmap, nikto, nuclei, sslyze, lynis
/security-pentest --server server4 --ssh-key ~/.ssh/xzopia_server1
# Backup before any significant change (e.g. the live MCP box)
/backup-server --server server4 --download
# Pre-flight check before setup
/pre-flight-check --server server4
Apply these rules before writing or editing any script under scripts/ or src/scripts/:
/spec-first — for complex new scripts only (multi-service setups, new server roles). Not required for simple fixes or targeted edits./security-pentest or manual security review — before committing any deployment script that touches auth, ports, or credentials./verification-before-completion — run a fresh verification command and read its output before declaring any task done.A PreToolUse hook in .claude/settings.json will prompt this checklist automatically when writing to scripts/ or src/scripts/.
Run through this list before committing any change that touches scripts, config, or infrastructure:
Secrets — full standing procedure: docs/security/secrets-handling.md
0600, env-style KEY=value at /etc/<service>/<purpose>.env.template documenting the shape is committed alongside (precedent: scripts/server3/*.template).env files on the server (mode 600, owned by deploy user)ADMIN_TOKEN) have every $ escaped as $$ in .env files~/.xzopia-deploy/server2-passwords.env is the local secrets store — never committedvault.xzopiasecure.com) as the canonical storeDocker
127.0.0.1:<host-port>:<container-port> — never 0.0.0.0 or bare <port>:<port>docker compose config --quiet produces no warnings before starting services.env change: docker compose stop <svc> && docker compose rm -f <svc> && docker compose up -d <svc>docker compose down && docker compose up -dApache vhosts (Server 2)
*:80) uses ProxyPass / http://127.0.0.1:<port>/ and ProxyPassReverseRequestHeader set X-Forwarded-Proto "https" and RequestHeader set X-Forwarded-Port "443"Header unset X-Powered-By and basic security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy)certbot --apache (not --nginx) for Apache-managed vhosts-le-ssl.conf) after certbot runs:sed -i 's|</VirtualHost>| Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"\n</VirtualHost>|' \
/etc/apache2/sites-available/<domain>-le-ssl.conf
apache2ctl configtest && systemctl reload apache2 after every vhost changeNginx vhosts (Server 4)
/etc/letsencrypt/live/... before cert existscertbot --nginx used for Nginx-managed vhostsssl_protocols TLSv1.2 TLSv1.3 set globally in /etc/nginx/nginx.conf (not just site-level)server_tokens off set in /etc/nginx/nginx.confStrict-Transport-Security "max-age=31536000; includeSubDomains") in the SSL server blockCaddy (Server 3 — read-only reference)
Strict-Transport-Security, X-Frame-Options, X-Content-Type-Options, Referrer-Policy) must be added explicitly via a header block in the Caddyfile — Caddy does not add them by defaultadmin off to the global block in the CaddyfileMCP services
mcp-proxy must be started with --host 127.0.0.1 to prevent binding to all interfaces0.0.0.0:<port> are HIGH severity — UFW is the only protection; add --host 127.0.0.1 to systemd ExecStartSSH / system hardening
ssh.socket disabled, ssh.service enabled before any sshd_config changes/run/sshd recreated after stopping ssh.socket and before sshd -tsshd_config Port and before enabling UFWsysctl.d/99-hardening.conf applied: bpf_jit_harden=2, send_redirects=0, log_martians=1/etc/fail2ban/jail.local — never edit jail.conf directly (overwritten on upgrade)22 in deployment scripts: grep -rn '"22"\|22/tcp\|port.*22\|22.*port' scripts/ src/Before committing reports
reports/ (gitignored). Use git add -f reports/<file> to force-add79.99.41.247) or Server 4 IP (77.68.112.217), bypass the pre-commit hook with ALLOW_SERVER3_CHANGES=1 / ALLOW_SERVER4_CHANGES=1 git commitEvery new server setup and every major change to live infrastructure must follow the 6-stage deploy pipeline. Do not skip stages. Do not mark a server as operational until all 6 stages pass.
Stage 0: docs/new-server-preparation.md → xzopia user + key + hostname + apt upgrade
Stage 1: /pre-flight-check --server <id> → READY verdict required
Stage 2: npm run dev -- setup-<server>-full → exit 0 + services running
Stage 3: /audit-server --server <id> → all health checks PASS
Stage 4: /security-pentest --server <id> → 0 CRITICAL, 0 HIGH
Stage 5: Update README.md + commit + push → status = Operational
Full documentation: .claude/skills/deploy-pipeline/SKILL.md
Server 4 (77.68.112.217) runs the live production MCP stack — 12 connectors at mcp.xzopiasecure.com. It is now the highest-risk MCP server in the stack — 350+ managed devices and all internal automation depend on it.
Before making any change to Server 4:
/backup-server --server server4 (or snapshot the VM)LESSONS_LEARNED.md with date and reasonsetup-server4-full against the running box — manual changes onlyscripts/server4/mcp-healthcheck.sh or curl each /mcp)Server 3 is the live Comet Server (Xzopia BaaS) — client-revenue backup infra; manual changes only, backup-first (same discipline as S4).
A pre-commit hook (.githooks/pre-commit) blocks any commit that adds the Server 3 or Server 4 IP to a non-documentation file. To bypass after taking a confirmed backup:
ALLOW_SERVER4_CHANGES=1 git commit -m "..." # or ALLOW_SERVER3_CHANGES=1 for S3
To activate the hook after a fresh clone:
git config core.hooksPath .githooks
config/servers.json is the only place server configuration is defined. All skills (pre-flight-check, audit-server, backup-server, security-pentest) read from it automatically.
Whenever any server detail changes (IP, port, hostname, subdomains, user), update config/servers.json first. Do not hardcode server details anywhere else.
"ip": "TBC" are skipped gracefully by all skills"readonly": true are skipped by backup-server and should never have setup scripts run against themWhen chat reports an MCP connector "failing to load" or showing no tools available, do NOT assume the S4 service is down. Verify before acting — twice now (Wiki.js, 1 Jul; BookStack, 30 Jun) the S4 process was healthy the whole time and the fault was a stale Claude.ai connector session, fixed by reconnecting the connector (toggle off/on or start a new chat), not by touching S4.
Diagnosis order (fast):
pm2 describe mcp-<svc> — check status: online, restarts (0 + long uptime ⇒ it never crashed).curl -s http://127.0.0.1:<port>/mcp (expect {"status":"ok",...}).curl -s https://mcp.xzopiasecure.com/<svc>/mcp and, ideally, a real MCP tools/list over the public URL.Server-side keepalive: pm2 already auto-restarts crashed MCP processes and persists across reboot (pm2 save + enabled systemd unit). A hung-but-listening process (endpoint stops responding while pm2 still shows online) is the gap pm2 misses — scripts/server4/mcp-healthcheck.sh (cron, every 5 min on S4) curls each /mcp endpoint and pm2 restarts only a service that fails twice in a row. S4 port map for the check is in config/servers.json / the port map below; mcp-m365 (3005) is intentionally excluded (abandoned legacy).
📖 Canonical standing procedure:
docs/security/secrets-handling.md(formalised 24 Jul 2026, also a standing rule indocs/session-protocol.md). It governs how a credential gets from its originating system onto a box and into CC's hands: never through any conversational interface, Vaultwarden as source of truth, root-owned0600/etc/<service>/<purpose>.envfiles, CC pre-creating the placeholder +.template, Simon inserting the value no-echo on-box, and confirmation by path only. Follow it by default — do not re-derive it.
Rule: no secret is ever committed to this repo.
| Secret type | Where it lives |
|---|---|
Server .env files |
On the server at /opt/xzopia/<server-id>/.env (mode 600) |
| Server passwords | Local ~/.xzopia-deploy/server2-passwords.env (mode 600) |
| SSH keys | ~/.ssh/xzopia_server1 (never in repo) |
| Long-term credentials | vault.xzopiasecure.com (Vaultwarden) |
| MCP API keys | /opt/xzopia/<server>/.env on the server, sourced from Vaultwarden |
Specific rules:
ADMIN_TOKEN must be an argon2id hash (bcrypt is treated as plain text in Vaultwarden 2025+).env value containing $ (argon2 hashes, bcrypt hashes, generated passwords) must have each $ escaped as $$ before writing to a Docker Compose .env file$→$$ escaping — bash heredoc $$ expands to PIDEvery server setup script lives in two places. Both must be kept in sync:
| Path | Purpose |
|---|---|
scripts/<server>/<name>.sh |
Canonical human-readable bash — can be run directly on the server |
src/scripts/<name>.ts |
Same content as a TypeScript string constant — bundled into dist/index.js by tsup |
When editing a setup script, always update both files. The .ts version uses \${VAR} wherever the bash script uses ${VAR} (to prevent TypeScript template literal expansion).
The TypeScript version is the one actually used by the CLI (npm run dev -- <command>). The .sh version is the human-readable reference and can be used for direct server-side execution.
Any script that gets pasted into the TacticalRMM script editor (PowerShell or bash deployed to endpoints — e.g. scripts/windows/teamviewer/*.ps1) must be pure ASCII. The TRMM editor does not preserve non-ASCII bytes: em-dashes (—), box-drawing comment dividers (──), smart quotes, and arrows (→) get mangled to ?, which breaks string literals and triggers a cascade of "missing terminator / missing closing brace" parse errors (lesson #101).
- for dashes and # --- for comment dividers; straight quotes " ' only.grep -P '[^\x00-\x7F]' <file> must return nothing; file <file> should report ASCII text.📖 Canonical session protocol + glossary + reference facts:
docs/session-protocol.md.
That page is the single source for the standing rules, the single-writer documentation rule, session hygiene, the boot/apply/patch-vs-rewrite operational procedure, the glossary (incl. theS1-EDR= SentinelOne / bareS1= Server 1 convention), and the stable REFERENCE facts (backup, patch/reboot, server topology, S4 port map, comms app, BookStack, TRMM pilot). CLAUDE.md and the session-handoff (wiki id=15) reference it — they do not duplicate it. Read it at the start of every session. Chat's volatile session state (LIVE / TO COMMIT / AGENDA / SIMON ACTIONS / boot line) stays in id=15.
Two agents write documentation for this project, with different access:
Two Claude Code instances (two-committer model, added 9 Jul 2026):
main (the established, calibrated instance).gh pr create), Simon reviews and merges. NOT direct push to main. This is precautionary during onboarding, not a trust judgment (Stephen has near-full fleet access already): his instance is brand new without the earned calibration, and it mirrors the stage→review→promote pattern already used for the wiki, applied to git. Git flags conflicts rather than silently overwriting, so two committers is lower-risk here than the wiki case was. Explicitly revisitable — once Stephen has a track record (Simon's call; ~a month or a dozen-ish clean PRs), direct push is reasonable to reconsider. Setup + rationale: docs/engineer-training-claude-setup.md (Part D) and the Access Register.1. docs/session-handoff.md (wiki page id=15) is the single chat↔Claude Code channel.
It is the ONE document where the wiki may temporarily be ahead of the repo. Chat writes session progress here live during chat-heavy sessions.
2. Chat writes ONLY to the handoff page.
Chat must not write any other wiki page directly. If chat needs to record a lesson, a status change, or a new doc, it writes it INTO the handoff under a clearly-marked TO COMMIT (Claude Code) list — it does not edit LESSONS_LEARNED.md, PROJECT_STATUS.md, runbooks, or any other doc directly.
3. Claude Code is the committer and reconciler. At the start (or end) of any session, Claude Code:
docs/session-handoff.md (wiki→repo for this one file — review diff first, additive only).TO COMMIT items from the handoff into their proper repo homes (LESSONS_LEARNED.md, PROJECT_STATUS.md, etc.).=== id=15 UPDATES TO APPLY === block (in the cc-report page AND the final chat message, identical, on every task — the dual copies are the reliability backstop, not redundancy) giving the exact edits; chat applies them. Chat's default consumption is to READ the block from the cc-report page (Simon says "apply from id=XX" → chat fetches the report, extracts, applies) — leaner than paste; the chat copy is the fallback if the report omits the block, and paste is the fallback if the Wiki.js MCP is down. "Reconcile id=15" in any instruction means steps a–b + emit that block — it never means CC writes the wiki id=15. Chat is the sole writer of id=15; this is the collision guarantee (see LESSONS_LEARNED #109).ListTenants finding (6 Jul); report id=88 re-read in full and caught that its own earlier same-day DR-runbook commit (c28a312) had gone stale, re-committing the current version instead (8 Jul); and — a different Claude surface applying the same discipline unprompted — a Cowork scheduled task noticed it was working from id=15's description of the DR runbook rather than the real file and flagged the exact fix (8 Jul). Fetch and read the whole current page before forming conclusions about project state. The same discipline generalises: verify against the primary source (repo / live system / real file), never a secondhand summary — id=15's TO COMMIT list can itself be stale (items already committed but not yet reconciled out).4. Everything except the handoff is strictly repo-first.
Lessons, status, runbooks, configs — written only via Claude Code, repo→wiki direction. Direct wiki edits to these pages get overwritten on the next push.
5. Reconcile cadence.
If a session was chat-heavy, the first Claude Code action next session is step 3 (reconcile handoff + promote TO COMMIT items) before doing anything else. This stops the repo drifting stale.
Rationale: chat can only propose (to the handoff); Claude Code commits (to the repo). One writer to the source of truth. The handoff is the explicit, version-tolerant buffer between them.
Wiki sync scope (.githooks/wiki-sync.py): CLAUDE.md, PROJECT_STATUS.md, LESSONS_LEARNED.md, FUTURE_DEVELOPMENT.md, and docs/*.md (top-level only). docs/migration/*.md is intentionally excluded — these are internal MSP planning docs (policy-assignment map, parity analysis, migration reports) and are repo-source-of-truth only, not client-KB material. Commits to docs/migration/ never trigger a wiki sync.
Cowork runbook mirror (.githooks/wiki-sync.py → mirror_runbooks_to_cowork()): docs/runbooks/*.md is not wiki-synced, but every commit touching it also copies the runbooks to a native Windows folder (C:\Users\SimonKillen.AzureAD\ClaudeCowork\xzopia\) so Claude Cowork can mount them as a live source. Cowork cannot mount WSL/UNC paths (\\wsl.localhost\… is rejected; net use fails with System error 64 — confirmed dead end, do not attempt alternatives). The mirror is a no-op on any clone/CI without that folder tree.
Two page types in the CC ↔ chat workflow:
Prompt pages (Chat → CC): Chat stages instructions as wiki pages; CC reads them by ID at task start. Index at id=43. Individual prompts at xzopia/cc-prompts/<YYYY-MM-DD>-<topic-slug> (date-first — see the naming convention in docs/session-protocol.md; older pages use cc-prompt-<task>). CC reads the page, executes, then writes a report page.
Report pages (CC → Chat): CC writes structured reports to wiki pages after completing research or before applying changes. Chat reads them directly via Wiki.js MCP — no copy-paste relay. Path convention: xzopia/cc-reports/<YYYY-MM-DD>-<topic-slug> (date-first, so the page list sorts chronologically — see the naming convention in docs/session-protocol.md; older pages use date-last).
Every report page must contain: (1) what CC did, (2) findings including unexpected discoveries and risks, (3) proposed changes as exact diff not summary, (4) blockers and questions, (5) next action once unblocked.
Rules (single writer per page — this is the collision guarantee):
=== id=15 UPDATES TO APPLY === block (in its cc-report page AND its final chat message) with exact edits (old → new, or "add under section X"); chat applies them.npm run dev # run CLI in development (via tsx, no build step)
npm run build # compile to dist/ with tsup (ESM + .d.ts)
npm run typecheck # tsc --noEmit, no output files
npm run lint # eslint src/
npm run lint:fix # eslint src/ --fix
npm run test # vitest run (single pass)
npm run test:watch # vitest watch mode
npm run test:single # vitest run --reporter=verbose (useful for one file)
The project is a Node.js ESM CLI tool built with TypeScript, compiled via tsup, and entry-pointed at src/index.ts.
Entry flow: src/index.ts creates a commander Command, registers subcommands imported from src/commands/, and calls program.parseAsync. Each command file exports a single Command instance.
Directory layout:
src/commands/ — one file per CLI subcommand, each exporting a Commandsrc/scripts/ — bash scripts embedded as TypeScript string constants (embedded to survive tsup bundling; see note below)src/utils/ — shared helpers (exec.ts wraps execa)src/types/ — shared TypeScript interfaces; index.ts re-exports everythingscripts/server1/ — canonical standalone bash scripts; kept for readability and direct server usetests/ — Vitest tests, mirroring src/ naming conventionsScript embedding pattern: Bash scripts that need to be SCP'd or piped to remote servers are stored in two places:
scripts/<server>/<name>.sh — the canonical, human-readable sourcesrc/scripts/<name>.ts — the same content as an exported TypeScript string constanttsup bundles src/ into a single dist/index.js, making filesystem path resolution for external scripts unreliable. Embedding scripts as TS constants avoids this. When editing a script, update both files. Use \${VAR} in the TS constant wherever the bash script uses ${VAR}.
Key dependencies:
commander — CLI argument/option parsingexeca — safe subprocess execution (preferred over child_process)chalk — terminal colour outputtsx — zero-build dev executiontsup — production bundler (wraps esbuild)vitest — test runnerModule system: The project uses "type": "module" (ESM). All internal imports must include the .js extension even for .ts source files — this is a Node ESM requirement and tsup/tsc both expect it.
Custom skills are in .claude/skills/. Invoke with /skill-name.
| Skill | When to use |
|---|---|
deploy-pipeline |
Mandatory for any new server setup or major change — 5-stage gate pipeline. Usage: /deploy-pipeline --server server4 |
pre-flight-check |
Stage 1 of deploy pipeline. Before any server setup. Usage: /pre-flight-check --server server1 |
setup-server1 |
Pre-flight checklist, step table, and recovery notes for Server 1 (TacticalRMM, Ubuntu 22.04). Usage: /setup-server1 |
setup-server4 |
Infrastructure setup for Server 4 (MCP Dev). Usage: /setup-server4 |
audit-server |
Stage 3 of deploy pipeline. After setup or before maintenance. Usage: /audit-server --server server2 |
backup-server |
Before any major change — Docker volumes, configs, DB dumps. Usage: /backup-server --server server2 --download |
| Skill | When to use | Source |
|---|---|---|
owasp-security |
Writing or reviewing auth, input handling, API design, LLM integrations | agamm/claude-code-owasp |
varlock |
Handling secrets, .env files, API keys, credentials |
wrsmith108/varlock-claude-skill — requires varlock CLI installed |
| Skill | When to use | Source |
|---|---|---|
systematic-debugging |
Any bug, test failure, or unexpected behaviour — before proposing fixes | obra/superpowers |
defense-in-depth |
After finding a data-related bug — add validation at every layer | obra/superpowers |
tdd |
Implement any feature using red-green-refactor. Usage: /tdd add JWT auth or /tdd plan.md |
mfranzon/tdd |
karpathy-guidelines |
Standing rules applied to every coding task — think first, minimal changes, surgical edits, verify goals | forrestchang/andrej-karpathy-skills |
spec-first |
Write a specification before implementing any non-trivial feature. Creates docs/specs/YYYY-MM-DD-name.md |
dceoy/speckit-agent-skills |
writing-plans |
Create a detailed implementation plan before coding multi-step tasks | obra/superpowers |
executing-plans |
Work through a written plan task by task with verification at each step | obra/superpowers |
dispatching-parallel-agents |
When multiple independent failures or tasks can be solved concurrently | obra/superpowers |
verification-before-completion |
Standing rule — run fresh verification before claiming any task is done | obra/superpowers |
finishing-a-development-branch |
At end of a branch — verify tests, merge/PR/discard with confirmation | obra/superpowers |
| Skill | When to use |
|---|---|
security-pentest |
Weekly external scan — nmap, nikto, nuclei, sslyze, lynis. Reads targets from config/servers.json. Usage: /security-pentest [--server server1] [--all] [--ssh-key ~/.ssh/xzopia_server1] |
All skills read server definitions from config/servers.json. To add a new server, update that file only — all skills pick it up automatically. Servers with "ip": "TBC" are skipped gracefully. Servers with "readonly": true are skipped by backup-server.
Reports are saved to reports/pentest-YYYY-MM-DD.md (gitignored — use git add -f to commit).