Skip to content
prod 352bb92
Browse

Set up shared hosting

Objective — bootstrap the per-server Admin-Server toolkit on a fresh hosting account so config drift detection, MD5 integrity checks, disk/inode alerts, PHP error-log scanning, domain inventory, log rotation, and alert channels (ops · deploys · backups) are defined once for every app on that account.


This is the account-scoped server-readiness playbook before CodeCanyon setup-new reaches deployment. Conceptual companion: Three-tier admin (handbook).

Once per account, then every app

Once per account, then every app 1. This account (Admin-Server readiness) → 2. App #1 (setup-new) → 3. App #N (reuse server tier) This account Admin-Server readiness App #1 setup-new App #N reuse server tier
flowchart LR
A["New hosting account"] --> B["Server readiness · this playbook"]
B --> C["setup-new · app 1"]
B --> D["setup-new · app N"]

Every task in this phase follows four operating rules:

RuleMeaning
Runs on the serverSSH into ~/Admin-Server/ on the hosting account — zero toolkit binaries on your laptop
Secrets stay server-sideNo webhook URL or SA token in laptop argv or agent chat — see Secrets discipline
IdempotentRe-run safe: git pull-friendly; baselines non-destructive; cron lines detected before duplicate add
Secret source of truthWebhooks and heartbeat URLs are created/stored first in the shared infrastructure 1Password vault recorded in Zaj-PROJECT.md (default example: General-Dev); runtime copies live only in gitignored server.env at chmod 600
#TaskPriorityType
0.1Clone Admin-Server toolkit + initial repo setupMUSTCLI (SSH)
0.2Download op 1Password CLI (no root)MUST after P9C-1CLI
0.3Bootstrap config/server.env from templateMUST👤 on server (nano)
0.4Make scripts executable + chmod 600 on server.envMUSTCLI
0.5Generate initial MD5 baselineMUSTCLI
0.6Run status.sh smoke testMUSTCLI
0.7Fire one synthetic alert through ops channelMUSTCLI
0.8Register Hostinger cron entries (6 lines, 5-field syntax)MUSTAPI or 👤 hPanel UI
0.9Discover server runtime pathsMUSTCLI
0.10First commit + push to per-server private repoMUSTCLI
0.11Document server in CLAUDE.local.mdSHOULDManual
PrerequisiteVerify
SSH public-key auth to the accountssh <SSH_ALIAS> 'echo ok' — no password prompt
Private GitHub repo Admin-Hostinger-<account>Empty repo OK for first push
Discord or Slack ops channelOps webhook ready for #server-alerts or the project’s equivalent; configure only through the secret-safe flow below
Shared infrastructure 1Password vaultRecord the actual vault name in Zaj-PROJECT.md; General-Dev is the default example, not a fixed requirement
Heartbeat provider decisionMonSpark, Healthchecks.io, Better Stack, Cronitor, UptimeRobot heartbeats, or N/A with reason

Run before any install work:

Terminal window
ssh <SSH_ALIAS> '
if [ -x ~/Admin-Server/scripts/status.sh ]; then
echo "Admin-Server present — run Verify existing next"
~/Admin-Server/scripts/status.sh
else
ls ~/Admin-Server/scripts/*.sh 2>/dev/null | head -10
fi
'
OutputAction
status.sh or scripts listedJump to Verify existing — skip setup steps 0.1–0.5 only after liveness passes
No such fileContinue with the setup tasks
Scripts but no lib/init.sh or no config/server.envLegacy layout — run Migrate legacy installs to v2 before cron or app reuse

Run this branch when ~/Admin-Server/ exists but uses the older layout (config/webhooks.conf, missing config/server.env, missing lib/init.sh, or scripts that do not write the expected logs). Migration is not a pass by itself; after it, run Verify existing.

  1. Normalize the config layout without deleting legacy files.

    Terminal window
    ssh <SSH_ALIAS> 'bash -s' <<'REMOTE'
    set -euo pipefail
    cd ~/Admin-Server
    mkdir -p config logs baselines
    if [ ! -f config/server.env ]; then
    cp config/server.env.example config/server.env
    chmod 600 config/server.env
    echo "Created config/server.env from example; edit it before alerting checks."
    fi
    [ -f config/webhooks.conf ] && echo "Legacy config/webhooks.conf kept as read-only fallback."
    test -d lib || echo "Missing lib/ — pull the Admin-Server v2 repo before continuing."
    git status --short
    REMOTE

    Expected: config/server.env exists at chmod 600; legacy webhooks.conf is not deleted; missing lib/ is treated as a blocker until the v2 repo is pulled.

  2. Complete the v2 gates. Edit server.env with the slot matrix in step 0.3, register absolute-path cron rows in step 0.8, regenerate the baseline, then run Verify existing. A migrated server does not become deploy-ready until the freshness probe has no STALE rows.

Verification-first flow: Clone → op CLI (P9C-1) → server.env → baseline → status + alert → cron liveness → runtime discovery → commit/push.

  1. Clone the per-server private repo into ~/Admin-Server/.

    Terminal window
    ssh <SSH_ALIAS> '
    git clone git@github.com-<GITHUB_USER>:<GITHUB_USER>/Admin-Hostinger-<account>.git ~/Admin-Server
    cd ~/Admin-Server && git remote -v
    '
    # Expected: origin → your Admin-Hostinger-<account> repo
  2. If the directory already exists, update instead of re-cloning.

    Terminal window
    ssh <SSH_ALIAS> 'cd ~/Admin-Server && git fetch && git pull'

Step 0.2 (download op 1Password CLI, no root) is MUST after P9C-1, not part of the interim bootstrap. For forward compatibility only, add an empty placeholder when editing server.env in step 0.3:

Terminal window
OP_SERVICE_ACCOUNT_TOKEN=""

Do not run op signin from an agent session.

  1. Create server.env from the template on the server only.

    Terminal window
    ssh <SSH_ALIAS> '
    cd ~/Admin-Server
    cp config/server.env.example config/server.env
    chmod 600 config/server.env
    nano config/server.env
    '
  2. Set identity and at least one ops webhook from the shared infrastructure vault (paste URL in nano on server — never into chat).

    Minimum alerting-ready fields: SERVER_NAME, SERVER_LABEL, SERVER_ENV (staging | production), plus DISCORD_WEBHOOK_OPS or SLACK_WEBHOOK_OPS. The source copy lives in 1Password, usually op://General-Dev/... unless Zaj-PROJECT.md names a different shared infrastructure vault.

SlotVariablesRequired?Behavior when blank
Server identitySERVER_NAME, SERVER_LABEL, SERVER_ENVRequiredstatus.sh cannot prove the account identity
Ops alertsDISCORD_WEBHOOK_OPS, SLACK_WEBHOOK_OPSRequired for alerting-readyMonitoring alerts log locally only; this playbook’s alert gate cannot pass
Deploy notificationsDISCORD_WEBHOOK_DEPLOYS, SLACK_WEBHOOK_DEPLOYSRequired when deploy notifications are enabledDeployer Section J logs “notification skipped” and continues
Backup notificationsDISCORD_WEBHOOK_BACKUPS, SLACK_WEBHOOK_BACKUPSRequired when backup alerting is enabledBackup alerts no-op or fall back to ops depending on the script
Legacy/default webhookWEBHOOK_URLLegacy fallback onlyCurrent v2 scripts should prefer the channel-specific vars above
External heartbeat pingsHEARTBEAT_SNAPSHOT, HEARTBEAT_INTEGRITY, HEARTBEAT_RESOURCE, HEARTBEAT_ERRORLOG, HEARTBEAT_INVENTORYRequired when external heartbeat monitoring is claimedCron still runs locally, but MonSpark/Healthchecks/Better Stack/Cronitor/UptimeRobot will not page on missed runs
Future 1Password fetchOP_SERVICE_ACCOUNT_TOKENEmpty until service-account hardeningserver.env remains the runtime copy; source values stay in the shared infra vault

Alert channels are part of the server setup, not a later nice-to-have. Use the channel names below as the default map, then record the actual workspace/channel names in Zaj-PROJECT.md.

If an agent is driving Discord, Slack, or MonSpark through the operator’s logged-in browser, use the vendor console automation guide: click by accessibility-tree element refs, not screenshot coordinates; let the human own login and secret-copy moments; move copied URLs through clipboard → stdin so webhook and heartbeat URLs never print in chat, terminal output, or process lists.

RouteCommon destinationVariables
Ops#server-alertsDISCORD_WEBHOOK_OPS or SLACK_WEBHOOK_OPS
Deploys#deploymentsDISCORD_WEBHOOK_DEPLOYS or SLACK_WEBHOOK_DEPLOYS
Backupsops or backup channelDISCORD_WEBHOOK_BACKUPS or SLACK_WEBHOOK_BACKUPS
App alerts#app-alertsApp/Sentry/MonSpark routing, not Admin-Server deploy vars
  1. Create or reuse provider webhooks in the right workspace.

    • Discord: right-click channel → Edit ChannelIntegrationsCreate Webhook → rename → SaveCopy URL.
    • Slack: api.slack.com/appsIncoming WebhooksAdd to Workspace → channel → Copy.
    • MonSpark heartbeats: Cron JobCreate Monitor → name/interval/grace → Copy heartbeat URL.
    • If the operator already has the right channel and webhook, reuse it; do not create duplicates.
  2. Store every URL in 1Password before it touches the server.

    Shared infra vault: <actual-vault-name> # default example: General-Dev
    op://<vault>/Admin-Server/SLACK_WEBHOOK_OPS
    op://<vault>/Admin-Server/DISCORD_WEBHOOK_OPS
    op://<vault>/Admin-Server/SLACK_WEBHOOK_DEPLOYS
    op://<vault>/Admin-Server/DISCORD_WEBHOOK_DEPLOYS
  3. Smoke-test endpoints without printing secrets.

    Terminal window
    DISCORD_URL="$(op read 'op://<vault>/Admin-Server/DISCORD_WEBHOOK_OPS')"
    curl -fsS -o /dev/null -w '%{http_code}\n' "$DISCORD_URL"
    unset DISCORD_URL
    # Expected: 200 for Discord webhook GET metadata
    SLACK_URL="$(op read 'op://<vault>/Admin-Server/SLACK_WEBHOOK_OPS')"
    printf '{"text":"Admin-Server webhook smoke test"}' \
    | curl -fsS -X POST -H 'Content-type: application/json' --data @- "$SLACK_URL"
    unset SLACK_URL
    # Expected: ok

    If a provider does not support a harmless preflight for that URL, skip the endpoint probe and rely on the real send_alert/alert-helper test after wiring.

  4. Paste into config/server.env through an interactive editor or secure clipboard bridge.

    Terminal window
    ssh <SSH_ALIAS> '
    cd ~/Admin-Server
    chmod 600 config/server.env
    nano config/server.env
    '

    Never echo webhook URLs into terminal logs, agent chat, README files, screenshots, or commits.

  5. Verify with the shipped alert helper.

    Terminal window
    ssh <SSH_ALIAS> '
    cd ~/Admin-Server
    if [ -x scripts/send_alert.sh ]; then
    bash scripts/send_alert.sh ops INFO "Admin-Server alerting smoke test for $(hostname)"
    else
    bash scripts/alert-helper.sh INFO "Admin-Server alerting smoke test for $(hostname)" ops
    fi
    '
    # Expected: exit 0 and the ops channel receives the message with the server env prefix.

0.4 · Executable scripts + server.env permissions

Section titled “0.4 · Executable scripts + server.env permissions”
  1. chmod scripts and confirm server.env stays private.

    Terminal window
    ssh <SSH_ALIAS> '
    chmod +x ~/Admin-Server/scripts/*.sh
    chmod 600 ~/Admin-Server/config/server.env
    ls -l ~/Admin-Server/scripts/status.sh ~/Admin-Server/config/server.env
    '
    # Expected: scripts -rwx; server.env -rw-------
  1. Generate and commit the integrity baseline.

    Terminal window
    ssh <SSH_ALIAS> '
    cd ~/Admin-Server
    conf="config/monitored-paths.conf"
    touch "$conf"
    grep -Fxq "$HOME/.ssh/authorized_keys" "$conf" 2>/dev/null || printf "%s\n" "$HOME/.ssh/authorized_keys" >> "$conf"
    ~/Admin-Server/scripts/generate-baseline.sh
    git add "$conf" baselines/checksums.md5
    git diff --cached --quiet || git commit -m "Initial integrity baseline"
    '
    # Expected: checksums.md5 with server dotfiles and ~/.ssh/authorized_keys
  1. Run the health snapshot — this is the verify step in the eight-step flow.

    Terminal window
    ssh <SSH_ALIAS> '~/Admin-Server/scripts/status.sh'

    Expected: clean SERVER_LABEL / SERVER_ENV; baseline ✅; webhook slots show ✅/❌ (at least one ✅ before you leave this playbook); cron schedule printed — use that output for the cron step.

0.7 · Fire one synthetic alert (ops channel)

Section titled “0.7 · Fire one synthetic alert (ops channel)”
  1. Send a test alert through the ops channel.

    Terminal window
    ssh <SSH_ALIAS> '
    ~/Admin-Server/scripts/alert-helper.sh INFO "Server-readiness smoke test" "Hello from $(hostname)" ops
    '

    Expected: Discord/Slack message with [STAGING] 🟡 or [PRODUCTION] 🔴 prefix.

0.8 · Register six Hostinger cron entries

Section titled “0.8 · Register six Hostinger cron entries”

Hostinger shared hosting often has no SSH crontab CLI. Use the Hostinger REST API when you have the account token; otherwise use hPanel → Advanced → Cron Jobs. Use SSH crontab only on hosts where command -v crontab succeeds.

Cron jobTypical scriptWatches
Config snapshotsnapshot-config.shTracked config drift
MD5 integrityintegrity-check.shBaseline mutations
Disk / inoderesource-check.shSpace and inode exhaustion
PHP error logerror-log-check.shNew PHP errors across domains
Domain inventorydomain-inventory.shApps/domains on account
Log rotationrotate-logs.shLog volume
  1. Resolve the absolute Admin-Server path before creating any cron row.

    Terminal window
    ssh <SSH_ALIAS> 'printf "ADMIN_SERVER=%s\n" "$HOME/Admin-Server"'
    # Expected: ADMIN_SERVER=/home/<user>/Admin-Server
  2. Copy cron expressions from status.sh output (step 0.6) — do not guess schedules.

  3. Create or verify the six cron rows with API GET-before-POST when available. Match the absolute command exactly.

    Terminal window
    # Example row payload shape; repeat for each script from the table above.
    # Do not include redirects, pipes, or $HOME in command.
    command="bash /home/<user>/Admin-Server/scripts/snapshot-config.sh"

    If the API is not available, add the same absolute commands in hPanel and verify five-field syntax (min hour dom mon dow). A UI preset is not enough; inspect the saved row.

  4. Confirm log files update after the first interval with the freshness probe.

    Terminal window
    ssh <SSH_ALIAS> 'bash -s' <<'REMOTE'
    set -euo pipefail
    cd ~/Admin-Server
    now=$(date +%s)
    check_log() {
    label="$1"; path="$2"; max_age="$3"
    if [ ! -f "$path" ]; then
    echo "❌ STALE $label — missing $path"; exit 1
    fi
    mtime=$(stat -c %Y "$path")
    age=$((now - mtime))
    if [ "$age" -gt "$max_age" ]; then
    echo "❌ STALE $label — last run $(date -d "@$mtime" -Is), age ${age}s, max ${max_age}s"; exit 1
    fi
    echo "✅ fresh $label — last run $(date -d "@$mtime" -Is)"
    }
    check_any_log() {
    label="$1"; max_age="$2"; shift 2
    for path in "$@"; do
    [ -f "$path" ] && check_log "$label" "$path" "$max_age" && return 0
    done
    echo "❌ STALE $label — missing expected log: $*"; exit 1
    }
    check_any_log "config snapshot" 172800 logs/snapshot-config.log
    check_any_log "integrity" 172800 logs/integrity.log logs/integrity-check.log
    check_any_log "disk/inode" 43200 logs/resource.log logs/resources.log logs/resource-check.log
    check_any_log "PHP error scan" 43200 logs/error-log.log logs/error-log-check.log
    check_any_log "domain inventory" 2592000 logs/domain-inventory.log
    check_any_log "log rotation" 1209600 logs/rotate-logs.log
    REMOTE

    Expected: every row prints ✅ fresh; any ❌ STALE row means the cron is not alive enough for app deploys.

External heartbeat wiring (MonSpark, Healthchecks, Better Stack, Cronitor, UptimeRobot)

Section titled “External heartbeat wiring (MonSpark, Healthchecks, Better Stack, Cronitor, UptimeRobot)”

External heartbeat monitors are layered on top of the local cron/log freshness gate. They are useful, but they are not a substitute for fresh Admin-Server logs. If the operator already pays for MonSpark, Better Stack, Cronitor, or UptimeRobot heartbeat monitors, use that tool here instead of signing up for a new Healthchecks.io account.

ScriptHeartbeat slotProvider examples
snapshot-config.shHEARTBEAT_SNAPSHOTMonSpark Cron Job · Healthchecks ping · Better Stack/Cronitor/UptimeRobot heartbeat
integrity-check.shHEARTBEAT_INTEGRITYMonSpark Cron Job · Healthchecks ping · Better Stack/Cronitor/UptimeRobot heartbeat
resource-check.shHEARTBEAT_RESOURCEMonSpark Cron Job · Healthchecks ping · Better Stack/Cronitor/UptimeRobot heartbeat
error-log-check.shHEARTBEAT_ERRORLOGMonSpark Cron Job · Healthchecks ping · Better Stack/Cronitor/UptimeRobot heartbeat
domain-inventory.shHEARTBEAT_INVENTORYMonSpark Cron Job · Healthchecks ping · Better Stack/Cronitor/UptimeRobot heartbeat

If a slot is blank, that script should still run locally but will not ping the external heartbeat provider. If you claim external heartbeat monitoring is configured, paste the heartbeat URLs into config/server.env, run the matching script once, and confirm the provider dashboard receives the ping. Leave blank slots explicitly documented as N/A — no external heartbeat configured.

For MonSpark specifically, create one Cron Job monitor per Admin-Server script and paste its URL into the matching HEARTBEAT_* slot. The MonSpark monitoring guide covers the full provider setup and optional Laravel MONSPARK_CRON_URL scheduler heartbeat.

  1. Record the server runtime paths the app deploys will consume.

    Terminal window
    ssh <SSH_ALIAS> 'bash -s' <<'REMOTE'
    set -euo pipefail
    cd ~/Admin-Server
    {
    echo "# Generated by setup-shared-hosting; update when hPanel PHP/Node changes."
    echo "PHP_LOGIN_SHELL=$(command -v php || true)"
    for phpbin in /opt/alt/php84/usr/bin/php /opt/alt/php83/usr/bin/php /opt/alt/php82/usr/bin/php /usr/local/bin/php /usr/bin/php; do
    [ -x "$phpbin" ] && printf 'PHP_CANDIDATE=%s # %s\n' "$phpbin" "$("$phpbin" -r 'echo PHP_VERSION;' 2>/dev/null || true)"
    done
    echo "COMPOSER_BIN=$(command -v composer || true)"
    echo "NODE_BIN=$(command -v node || true)"
    echo "NPM_BIN=$(command -v npm || true)"
    } > config/server-environment.conf
    cat config/server-environment.conf
    git add config/server-environment.conf
    git diff --cached --quiet || git commit -m "Record server environment paths"
    REMOTE

    Expected: config/server-environment.conf is committed and shows the login-shell PHP separately from versioned PHP candidates. Later deploy pages still confirm the domain’s selected web/CLI PHP before pinning Composer.

  1. Push the Admin-Server repo (baseline + monitored paths + committed config such as server-environment.conf).

    Terminal window
    ssh <SSH_ALIAS> '
    cd ~/Admin-Server
    git status
    git push
    '

Record in the project’s CLAUDE.local.md (or equivalent local-only notes): SSH alias, Admin-Hostinger-<account> repo name, last baseline date, last webhook rotation — not in git-tracked secrets.


When detect-and-skip found scripts already installed:

Terminal window
ssh <SSH_ALIAS> 'bash -s' <<'REMOTE'
set -euo pipefail
cd ~/Admin-Server
./scripts/status.sh
now=$(date +%s)
check_log() {
label="$1"; path="$2"; max_age="$3"
if [ ! -f "$path" ]; then echo "❌ STALE $label — missing $path"; exit 1; fi
mtime=$(stat -c %Y "$path")
age=$((now - mtime))
if [ "$age" -gt "$max_age" ]; then
echo "❌ STALE $label — last run $(date -d "@$mtime" -Is), age ${age}s, max ${max_age}s"; exit 1
fi
echo "✅ fresh $label — last run $(date -d "@$mtime" -Is)"
}
check_any_log() {
label="$1"; max_age="$2"; shift 2
for path in "$@"; do
[ -f "$path" ] && check_log "$label" "$path" "$max_age" && return 0
done
echo "❌ STALE $label — missing expected log: $*"; exit 1
}
check_any_log "config snapshot" 172800 logs/snapshot-config.log
check_any_log "integrity" 172800 logs/integrity.log logs/integrity-check.log
check_any_log "disk/inode" 43200 logs/resource.log logs/resources.log logs/resource-check.log
check_any_log "PHP error scan" 43200 logs/error-log.log logs/error-log-check.log
check_any_log "domain inventory" 2592000 logs/domain-inventory.log
check_any_log "log rotation" 1209600 logs/rotate-logs.log
REMOTE

Fix only failed checks (missing baseline → 0.5; stale logs → 0.8; unset webhooks → 0.3 + 0.7). Then return to your app playbook.

If this verification is happening for a specific app domain, add that domain path to the integrity baseline before you return:

Terminal window
APP_DOMAIN="<domain.example.com>"
ssh <SSH_ALIAS> "APP_DOMAIN='$APP_DOMAIN' bash -s" <<'REMOTE'
set -euo pipefail
APP_DOMAIN="${APP_DOMAIN:?set APP_DOMAIN before running}"
cd ~/Admin-Server
path="$HOME/domains/$APP_DOMAIN"
conf="config/monitored-paths.conf"
touch "$conf"
for monitored in "$HOME/.ssh/authorized_keys" "$path"; do
grep -Fxq "$monitored" "$conf" 2>/dev/null || printf '%s\n' "$monitored" >> "$conf"
grep -Fx "$monitored" "$conf"
done
./scripts/generate-baseline.sh
git add "$conf" baselines/checksums.md5
git diff --cached --quiet || git commit -m "Monitor $APP_DOMAIN domain path"
REMOTE
# Expected: monitored-paths.conf contains authorized_keys + the domain path; baseline regenerated
ConcernOn second run
Repogit fetch && git pull — no re-clone
CronDetected before add — no duplicates
BaselineRegeneration non-destructive
Healthstatus.sh + the log freshness probe
  • Webhook URLs are credentials — live only in 1Password and gitignored server.env; never in README files, examples, commits, screenshots, or chat.
  • A committed webhook URL is compromised. Delete/recreate the webhook, then update 1Password and server.env; editing nearby config is not recovery.
  • Before committing Admin-Server docs/config examples, run rg -n 'discord\.com/api/webhooks/[0-9]+/[A-Za-z0-9._-]+' . and confirm there are no tracked Discord webhook URLs.
  • server.env: chmod 600, gitignored — interim SoT for ops / deploys / backups.
  • Prefer server-to-server copy or clipboard bridge at keyboard — never through agent chat.
  • P9C-1 (future): 1Password SA fetch replaces plaintext URLs — see Hardening.
WeaknessInterimAfter P9C-1
Plaintext webhooks on diskchmod 600 + gitignoreSA fetch at runtime
No audit trailRotation log in monitoring state1Password activity log
Per-server manual rotationOperator checklistRotate vault once; servers pull fresh

Later hardening: move Discord/Slack webhook URLs out of plaintext server.env into a scoped read-only 1Password service-account fetch that fails closed. This is post-launch Admin-Server ops work, not a setup-new Phase 9 launch gate.

P9C-1 unblocks after the CodeCanyon payments setup proves op on Hostinger CloudLinux.


Verification checklist:

  • SSH alias works passwordlessly from laptop
  • Server GitHub auth: ssh -T git@github.com-<GITHUB_USER> succeeds
  • ~/Admin-Server/ present; git remote -v correct
  • All monitoring scripts executable (-rwx)
  • config/server.env exists at chmod 600, gitignored
  • Six cron entries installed through Hostinger API/hPanel or SSH crontab when available; each row uses bash /home/<user>/Admin-Server/scripts/<script>.sh with no $HOME, pipes, or redirects
  • Admin-Server logs are fresh within each cron interval ×2; no STALE rows
  • baselines/checksums.md5 exists with ≥4 entries
  • Test alert received with [STAGING] 🟡 or [PRODUCTION] 🔴 prefix
  • Adopted external heartbeat slots show fresh provider pings, or each blank slot is recorded as N/A with reason
  • config/monitored-paths.conf includes ~/.ssh/authorized_keys and this app’s domain path before reuse
  • config/server-environment.conf committed with PHP/Node/Composer path candidates
  • Server documented in CLAUDE.local.md (step 0.11)

Source archives and intake ledgers stay out of reader-facing playbooks. The workflow above is the published server-readiness path.

When the gate passes, continue with CodeCanyon setup-new — Phase 1 workspace setup on your laptop.