[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6GDF2VNE95ZCX3M8YR6W1ZC":3,"skill-same:01M6GDF2VNE95ZCX3M8YR6W1ZC":73},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":23,"publishedAt":36,"linkOnly":37,"markdownContent":38,"markdownBody":39,"frontMatter":40,"refName":51,"commitSha":52,"skillDirectory":53,"forksCount":54,"repositoryHtmlUrl":55,"repositoryHomepageUrl":6,"ownerHtmlUrl":56,"ownerBlogUrl":6,"ownerDescription":57,"updatedAt":58,"install":59,"resources":72},"01M6GDF2VNE95ZCX3M8YR6W1ZC","hermes-s6-container-supervision",null,"Modify or debug s6 services in the Hermes Docker image.","zapabob\u002FSkills","zapabob","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F130830376?v=4","01M6DBMYZAGRTFQDSQYW3W764P","峯岸 亮","01M6DBMZ3GKDSJ01NZ9XTZD7JD",2,"MIT","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Farchive\u002Fskill-md-reviewed\u002Fsha256\u002Fb9\u002Fb9b65b6d74ef9d9fee60813700bf93da7bb2f28d1f952686c82c178ebe158246\u002FSKILL.md",[18],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署","SCENARIO","使用场景",[24,25,26,27,28,29,30,31,32,33,34,35],"hermes","container","hermes s6 container supervision","image","docker","linux","debug","gateway","services","hermes agent","hermes-agent","profiles","2026-11-10 10:12:30",false,"---\nname: hermes-s6-container-supervision\ndescription: Modify or debug s6 services in the Hermes Docker image.\nversion: 1.0.0\nauthor: Hermes Agent\nlicense: MIT\nplatforms: [linux]\nenvironments: [s6]\nmetadata:\n  hermes:\n    tags: [docker, s6, supervision, gateway, profiles]\n    related_skills: [hermes-agent]\n---\n\n# Hermes s6-overlay Container Supervision\n\n## When to use this skill\n\nLoad this skill when you're working on:\n- Adding or removing a static service in the Hermes Docker image (something that should be supervised at every container start, like the dashboard)\n- Diagnosing why a per-profile gateway isn't starting, restarting, or surviving `docker restart`\n- Understanding why the container's CMD is `\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh` and how leading-dash args reach the user's program\n- Modifying `cont-init.d` boot scripts (UID remap, volume seeding, profile reconciliation)\n- Changing the rendered run-script for per-profile gateways (Phase 4)\n\nIf you're just running the Hermes Agent and want to use Docker, see `website\u002Fdocs\u002Fuser-guide\u002Fdocker.md` instead.\n\n## Architecture at a glance\n\n```\n\u002Finit                                  ← PID 1 (s6-overlay v3.2.3.0)\n├── cont-init.d                        ← oneshot setup, runs as root\n│   ├── 01-hermes-setup                ← docker\u002Fstage2-hook.sh\n│   │   ├── UID\u002FGID remap\n│   │   ├── chown \u002Fopt\u002Fdata\n│   │   ├── chown \u002Fopt\u002Fdata\u002Fprofiles (every boot)\n│   │   ├── seed .env \u002F config.yaml \u002F SOUL.md\n│   │   └── skills_sync.py\n│   └── 02-reconcile-profiles          ← hermes_cli.container_boot\n│       ├── chown \u002Frun\u002Fservice (hermes-writable for runtime register)\n│       └── walk $HERMES_HOME\u002Fprofiles\u002F\u003Cname>\u002Fgateway_state.json\n│           → recreate \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\u002F\n│           → auto-start only those with prior_state == \"running\"\n│\n├── s6-rc.d (static services, in \u002Fetc\u002Fs6-overlay\u002Fs6-rc.d\u002F)\n│   ├── main-hermes\u002Frun                ← exec sleep infinity (no-op slot)\n│   └── dashboard\u002Frun                  ← if HERMES_DASHBOARD=1, runs `hermes dashboard`\n│\n├── \u002Frun\u002Fservice (s6-svscan watches; tmpfs)\n│   ├── gateway-coder\u002F                 ← runtime-registered per-profile\n│   │   ├── type        (\"longrun\")\n│   │   ├── run         (\"#!\u002Fcommand\u002Fwith-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run\")\n│   │   ├── down        (marker — present means \"registered but don't auto-start\")\n│   │   └── log\u002Frun     (s6-log → $HERMES_HOME\u002Flogs\u002Fgateways\u002Fcoder\u002Fcurrent)\n│   └── ...\n│\n└── CMD (\"main program\")               ← \u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\n    └── routes user args: bare exec | hermes subcommand | hermes (no args)\n        — exec'd by \u002Finit with stdin\u002Fstdout\u002Fstderr inherited (TTY for --tui)\n```\n\n## Key files\n\n| Path | Role |\n|---|---|\n| `Dockerfile` | s6-overlay install + cont-init.d wiring + `ENTRYPOINT [\"\u002Fopt\u002Fhermes\u002Fdocker\u002Fentrypoint-dispatch.sh\"]` |\n| `docker\u002Fentrypoint-dispatch.sh` | PID-1 dispatcher: exec's `\u002Finit` + main-wrapper when the image owns PID 1; on wrapped runtimes (Fly Machines, `docker run --init`) falls back to stage2-hook + main-wrapper directly, restoring the s6 helper PATH first (#38349). |\n| `docker\u002Fstage2-hook.sh` | The \"old entrypoint logic\" — UID remap, chown, seed, skills sync. Runs as cont-init.d\u002F01-hermes-setup. |\n| `docker\u002Fcont-init.d\u002F02-reconcile-profiles` | Calls `hermes_cli.container_boot` on every boot to restore profile gateway slots from the persistent volume. |\n| `docker\u002Fmain-wrapper.sh` | The container's CMD. Routes user args, drops to hermes via `s6-setuidgid`, exec's the chosen program. |\n| `docker\u002Fs6-rc.d\u002Fmain-hermes\u002Frun` | No-op `sleep infinity` — slot exists so the s6-rc user bundle is valid; main hermes runs as the CMD, not as a supervised service. |\n| `docker\u002Fs6-rc.d\u002Fdashboard\u002Frun` | Conditional service — `exec sleep infinity` unless `HERMES_DASHBOARD` is truthy. |\n| `docker\u002Fentrypoint.sh` | Back-compat shim that `exec`s the stage2 hook. External scripts that hard-coded the old entrypoint path still work. |\n| `hermes_cli\u002Fservice_manager.py` | `S6ServiceManager`: `register_profile_gateway`, `unregister_profile_gateway`, `start\u002Fstop\u002Frestart\u002Fis_running`, `list_profile_gateways`. |\n| `hermes_cli\u002Fcontainer_boot.py` | `reconcile_profile_gateways()` — walks persistent profiles, regenerates s6 slots, emits `container-boot.log`. |\n| `hermes_cli\u002Fgateway.py::_dispatch_via_service_manager_if_s6` | Intercepts `hermes gateway start\u002Fstop\u002Frestart` and routes to s6 when running in a container. |\n\n## Why Architecture B (CMD as main program, not s6-supervised)\n\nThe original plan (v1–v3) called for main hermes to run as a supervised s6-rc service. Two real s6-overlay v3 mechanics blocked that:\n\n1. **cont-init.d scripts receive no CMD args** — so the stage2 hook can't parse `docker run \u003Cimage> chat -q \"hi\"` to set `HERMES_ARGS` for a service `run` script to consume.\n2. **`\u002Frun\u002Fs6\u002Fbasedir\u002Fbin\u002Fhalt` does NOT propagate the exit code** written to `\u002Frun\u002Fs6-linux-init-container-results\u002Fexitcode`. Containers always exit 143 (SIGTERM) regardless. Confirmed by skarnet (s6 author) in [issue #477](https:\u002F\u002Fgithub.com\u002Fjust-containers\u002Fs6-overlay\u002Fissues\u002F477): _\"if you want a container shutdown, you need to either have your CMD exit, or, if you have no CMD, write the container exit code you want then call halt\"_.\n\nSo we use the s6-overlay-native CMD pattern via the dispatcher: `ENTRYPOINT [\"\u002Fopt\u002Fhermes\u002Fdocker\u002Fentrypoint-dispatch.sh\"]`, which under PID 1 exec's `\u002Finit \u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh \"$@\"`. The wrapper is prepended to user args automatically — so `docker run \u003Cimage> --version` becomes `\u002Finit main-wrapper.sh --version`, and `--version` doesn't get intercepted by \u002Finit's POSIX shell. The wrapper drops to hermes via `s6-setuidgid`, then exec's the chosen program. The program's exit code becomes the container exit code, exactly matching the pre-s6 tini contract. When the entrypoint is NOT PID 1 (Fly Machines, `docker run --init`), the dispatcher skips `\u002Finit` entirely (it would abort with `can only run as pid 1`), restores the s6 helper PATH, runs stage2-hook.sh, and exec's main-wrapper.sh directly — no supervised services on that path (#38349).\n\nTrade-off: main hermes is unsupervised under s6. That exactly matches its behavior under tini (the pre-s6 image). Dashboard supervision is the only **new** guarantee — and per-profile gateways under `\u002Frun\u002Fservice\u002F` get full supervision.\n\n## Quick recipes\n\n### Verify s6 is PID 1 in a running container\n\n```sh\ndocker exec \u003Cc> sh -c 'cat \u002Fproc\u002F1\u002Fcomm; readlink \u002Fproc\u002F1\u002Fexe'\n# Expect: s6-svscan or init \u002F \u002Fpackage\u002Fadmin\u002Fs6\u002F...\u002Fs6-svscan\n```\n\n### Inspect a profile gateway service\n\n```sh\n# \u002Fcommand\u002F isn't on docker-exec PATH — use absolute path\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svstat \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\n# \"up (pid …) … seconds\"            → running\n# \"down (exitcode N) … seconds, normally up, want up, …\" → s6 wants it up but the process keeps exiting (crash loop)\n# \"down … normally up, ready …\"     → user stopped it\n```\n\n### Bring a service up\u002Fdown manually\n\n```sh\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -u \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # up\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -d \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # down\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -t \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # SIGTERM (restart)\n```\n\n### Watch the cont-init reconciler log\n\n```sh\ndocker exec \u003Cc> tail -n 50 \u002Fopt\u002Fdata\u002Flogs\u002Fcontainer-boot.log\n# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started\n# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered\n```\n\n### Add a new static service\n\n1. Create `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Ftype` with `longrun\\n` and `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Frun` (use `#!\u002Fcommand\u002Fwith-contenv sh` + `# shellcheck shell=sh`).\n2. Drop to hermes via `s6-setuidgid hermes` at the top of run (unless you specifically need root).\n3. Create empty `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Fdependencies.d\u002Fbase` so it waits for the base bundle.\n4. Create empty `docker\u002Fs6-rc.d\u002Fuser\u002Fcontents.d\u002F\u003Cname>` so it joins the user bundle.\n5. The `COPY docker\u002Fs6-rc.d\u002F` in the Dockerfile picks it up automatically — no other changes.\n\n### Change the per-profile gateway run command\n\nEdit `S6ServiceManager._render_run_script` in `hermes_cli\u002Fservice_manager.py`. The function is also called by `hermes_cli\u002Fcontainer_boot.py::_register_service` during boot reconciliation, so it's the single source of truth. Update the corresponding assertion in `tests\u002Fhermes_cli\u002Ftest_service_manager.py::test_s6_register_creates_service_dir_and_triggers_scan`.\n\n### Run the docker test harness\n\n```sh\ndocker build -t hermes-agent-harness:latest .\nHERMES_TEST_IMAGE=hermes-agent-harness:latest scripts\u002Frun_tests.sh tests\u002Fdocker\u002F -v\n# Expect 19 passed, 0 xfailed against the s6 image\n```\n\nThe harness lives in `tests\u002Fdocker\u002F` and skips when Docker isn't available. The per-test timeout is bumped to 180s (see `tests\u002Fdocker\u002Fconftest.py`).\n\n## Common pitfalls\n\n### \"command not found\" via `docker exec`\n\n`\u002Fcommand\u002F` (where s6-overlay puts its binaries) is on PATH only for processes spawned by the supervision tree — services, cont-init.d, main-wrapper.sh. `docker exec \u003Cc> s6-svstat …` will fail with \"command not found\"; always use the absolute path `\u002Fcommand\u002Fs6-svstat`. The `hermes` binary works because the Dockerfile adds `\u002Fopt\u002Fhermes\u002F.venv\u002Fbin` to the runtime `ENV PATH`.\n\n### Profile directory ownership\n\nThe cont-init reconciler runs as hermes (`s6-setuidgid hermes` in `02-reconcile-profiles`). If a profile dir ends up root-owned (e.g. because `docker exec \u003Cc> hermes profile create …` ran as root by default), the reconciler can't read SOUL.md and fails with `PermissionError`. Mitigation: `stage2-hook.sh` chowns `$HERMES_HOME\u002Fprofiles` to hermes on **every** boot, idempotently. Don't remove that block.\n\n### Files written by `docker exec` are root-owned\n\n`docker exec` defaults to root. Either pass `--user hermes` or rely on the stage2 chown sweep next reboot. Don't write files under `$HERMES_HOME\u002Fprofiles\u002F\u003Cname>\u002F` as root manually — the next reconcile pass will sweep them but in-flight operations may hit perm errors.\n\n### Service slot exists but s6-svstat says \"s6-supervise not running\"\n\nThe service directory is on tmpfs and was wiped on container restart. Either the cont-init reconciler hasn't run yet (give it a moment after `docker restart`) or it failed. Check `docker logs \u003Cc> | grep '02-reconcile'`.\n\n### Gateway starts then immediately exits (`down (exitcode 1)` in svstat)\n\nMost likely the profile has no model or auth configured. The service slot is correct — the gateway itself is unconfigured. Run `hermes -p \u003Cprofile> setup` first. The s6 supervisor will keep restarting it; that's the desired behavior (when you fix the config, the next attempt succeeds and stays up).\n\n### Reconciler skipped a profile\n\nThe reconciler keys on the **presence of `SOUL.md`** as the \"real profile\" marker. `hermes profile create` always seeds it. If a profile dir is missing SOUL.md (stray directory, partial restore, backup-in-progress), the reconciler skips it intentionally. Add a `SOUL.md` (even empty) to opt back in.\n\n### \"Help, the container exits 143!\"\n\nCheck whether something is invoking `s6-svscanctl -t` or `\u002Frun\u002Fs6\u002Fbasedir\u002Fbin\u002Fhalt` — both cause \u002Finit to begin stage 3 shutdown but return 143 (SIGTERM) rather than the desired exit code. This was the Phase 2 architecture pivot from A to B. For container shutdown with a real exit code, you must let the CMD (main-wrapper.sh) exit normally; do **not** try to control exit from a finish script.\n\n## Related skills\n\n- `hermes-agent-dev`: General hermes-agent codebase navigation\n- `hermes-tool-quirks`: Specific Hermes-tool workarounds (sed\u002Fgrep\u002Fetc.) — load when debugging the s6 stack's interaction with hermes built-in tools.\n","# Hermes s6-overlay Container Supervision\n\n## When to use this skill\n\nLoad this skill when you're working on:\n- Adding or removing a static service in the Hermes Docker image (something that should be supervised at every container start, like the dashboard)\n- Diagnosing why a per-profile gateway isn't starting, restarting, or surviving `docker restart`\n- Understanding why the container's CMD is `\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh` and how leading-dash args reach the user's program\n- Modifying `cont-init.d` boot scripts (UID remap, volume seeding, profile reconciliation)\n- Changing the rendered run-script for per-profile gateways (Phase 4)\n\nIf you're just running the Hermes Agent and want to use Docker, see `website\u002Fdocs\u002Fuser-guide\u002Fdocker.md` instead.\n\n## Architecture at a glance\n\n```\n\u002Finit                                  ← PID 1 (s6-overlay v3.2.3.0)\n├── cont-init.d                        ← oneshot setup, runs as root\n│   ├── 01-hermes-setup                ← docker\u002Fstage2-hook.sh\n│   │   ├── UID\u002FGID remap\n│   │   ├── chown \u002Fopt\u002Fdata\n│   │   ├── chown \u002Fopt\u002Fdata\u002Fprofiles (every boot)\n│   │   ├── seed .env \u002F config.yaml \u002F SOUL.md\n│   │   └── skills_sync.py\n│   └── 02-reconcile-profiles          ← hermes_cli.container_boot\n│       ├── chown \u002Frun\u002Fservice (hermes-writable for runtime register)\n│       └── walk $HERMES_HOME\u002Fprofiles\u002F\u003Cname>\u002Fgateway_state.json\n│           → recreate \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\u002F\n│           → auto-start only those with prior_state == \"running\"\n│\n├── s6-rc.d (static services, in \u002Fetc\u002Fs6-overlay\u002Fs6-rc.d\u002F)\n│   ├── main-hermes\u002Frun                ← exec sleep infinity (no-op slot)\n│   └── dashboard\u002Frun                  ← if HERMES_DASHBOARD=1, runs `hermes dashboard`\n│\n├── \u002Frun\u002Fservice (s6-svscan watches; tmpfs)\n│   ├── gateway-coder\u002F                 ← runtime-registered per-profile\n│   │   ├── type        (\"longrun\")\n│   │   ├── run         (\"#!\u002Fcommand\u002Fwith-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run\")\n│   │   ├── down        (marker — present means \"registered but don't auto-start\")\n│   │   └── log\u002Frun     (s6-log → $HERMES_HOME\u002Flogs\u002Fgateways\u002Fcoder\u002Fcurrent)\n│   └── ...\n│\n└── CMD (\"main program\")               ← \u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\n    └── routes user args: bare exec | hermes subcommand | hermes (no args)\n        — exec'd by \u002Finit with stdin\u002Fstdout\u002Fstderr inherited (TTY for --tui)\n```\n\n## Key files\n\n| Path | Role |\n|---|---|\n| `Dockerfile` | s6-overlay install + cont-init.d wiring + `ENTRYPOINT [\"\u002Fopt\u002Fhermes\u002Fdocker\u002Fentrypoint-dispatch.sh\"]` |\n| `docker\u002Fentrypoint-dispatch.sh` | PID-1 dispatcher: exec's `\u002Finit` + main-wrapper when the image owns PID 1; on wrapped runtimes (Fly Machines, `docker run --init`) falls back to stage2-hook + main-wrapper directly, restoring the s6 helper PATH first (#38349). |\n| `docker\u002Fstage2-hook.sh` | The \"old entrypoint logic\" — UID remap, chown, seed, skills sync. Runs as cont-init.d\u002F01-hermes-setup. |\n| `docker\u002Fcont-init.d\u002F02-reconcile-profiles` | Calls `hermes_cli.container_boot` on every boot to restore profile gateway slots from the persistent volume. |\n| `docker\u002Fmain-wrapper.sh` | The container's CMD. Routes user args, drops to hermes via `s6-setuidgid`, exec's the chosen program. |\n| `docker\u002Fs6-rc.d\u002Fmain-hermes\u002Frun` | No-op `sleep infinity` — slot exists so the s6-rc user bundle is valid; main hermes runs as the CMD, not as a supervised service. |\n| `docker\u002Fs6-rc.d\u002Fdashboard\u002Frun` | Conditional service — `exec sleep infinity` unless `HERMES_DASHBOARD` is truthy. |\n| `docker\u002Fentrypoint.sh` | Back-compat shim that `exec`s the stage2 hook. External scripts that hard-coded the old entrypoint path still work. |\n| `hermes_cli\u002Fservice_manager.py` | `S6ServiceManager`: `register_profile_gateway`, `unregister_profile_gateway`, `start\u002Fstop\u002Frestart\u002Fis_running`, `list_profile_gateways`. |\n| `hermes_cli\u002Fcontainer_boot.py` | `reconcile_profile_gateways()` — walks persistent profiles, regenerates s6 slots, emits `container-boot.log`. |\n| `hermes_cli\u002Fgateway.py::_dispatch_via_service_manager_if_s6` | Intercepts `hermes gateway start\u002Fstop\u002Frestart` and routes to s6 when running in a container. |\n\n## Why Architecture B (CMD as main program, not s6-supervised)\n\nThe original plan (v1–v3) called for main hermes to run as a supervised s6-rc service. Two real s6-overlay v3 mechanics blocked that:\n\n1. **cont-init.d scripts receive no CMD args** — so the stage2 hook can't parse `docker run \u003Cimage> chat -q \"hi\"` to set `HERMES_ARGS` for a service `run` script to consume.\n2. **`\u002Frun\u002Fs6\u002Fbasedir\u002Fbin\u002Fhalt` does NOT propagate the exit code** written to `\u002Frun\u002Fs6-linux-init-container-results\u002Fexitcode`. Containers always exit 143 (SIGTERM) regardless. Confirmed by skarnet (s6 author) in [issue #477](https:\u002F\u002Fgithub.com\u002Fjust-containers\u002Fs6-overlay\u002Fissues\u002F477): _\"if you want a container shutdown, you need to either have your CMD exit, or, if you have no CMD, write the container exit code you want then call halt\"_.\n\nSo we use the s6-overlay-native CMD pattern via the dispatcher: `ENTRYPOINT [\"\u002Fopt\u002Fhermes\u002Fdocker\u002Fentrypoint-dispatch.sh\"]`, which under PID 1 exec's `\u002Finit \u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh \"$@\"`. The wrapper is prepended to user args automatically — so `docker run \u003Cimage> --version` becomes `\u002Finit main-wrapper.sh --version`, and `--version` doesn't get intercepted by \u002Finit's POSIX shell. The wrapper drops to hermes via `s6-setuidgid`, then exec's the chosen program. The program's exit code becomes the container exit code, exactly matching the pre-s6 tini contract. When the entrypoint is NOT PID 1 (Fly Machines, `docker run --init`), the dispatcher skips `\u002Finit` entirely (it would abort with `can only run as pid 1`), restores the s6 helper PATH, runs stage2-hook.sh, and exec's main-wrapper.sh directly — no supervised services on that path (#38349).\n\nTrade-off: main hermes is unsupervised under s6. That exactly matches its behavior under tini (the pre-s6 image). Dashboard supervision is the only **new** guarantee — and per-profile gateways under `\u002Frun\u002Fservice\u002F` get full supervision.\n\n## Quick recipes\n\n### Verify s6 is PID 1 in a running container\n\n```sh\ndocker exec \u003Cc> sh -c 'cat \u002Fproc\u002F1\u002Fcomm; readlink \u002Fproc\u002F1\u002Fexe'\n# Expect: s6-svscan or init \u002F \u002Fpackage\u002Fadmin\u002Fs6\u002F...\u002Fs6-svscan\n```\n\n### Inspect a profile gateway service\n\n```sh\n# \u002Fcommand\u002F isn't on docker-exec PATH — use absolute path\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svstat \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\n# \"up (pid …) … seconds\"            → running\n# \"down (exitcode N) … seconds, normally up, want up, …\" → s6 wants it up but the process keeps exiting (crash loop)\n# \"down … normally up, ready …\"     → user stopped it\n```\n\n### Bring a service up\u002Fdown manually\n\n```sh\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -u \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # up\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -d \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # down\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -t \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # SIGTERM (restart)\n```\n\n### Watch the cont-init reconciler log\n\n```sh\ndocker exec \u003Cc> tail -n 50 \u002Fopt\u002Fdata\u002Flogs\u002Fcontainer-boot.log\n# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started\n# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered\n```\n\n### Add a new static service\n\n1. Create `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Ftype` with `longrun\\n` and `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Frun` (use `#!\u002Fcommand\u002Fwith-contenv sh` + `# shellcheck shell=sh`).\n2. Drop to hermes via `s6-setuidgid hermes` at the top of run (unless you specifically need root).\n3. Create empty `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Fdependencies.d\u002Fbase` so it waits for the base bundle.\n4. Create empty `docker\u002Fs6-rc.d\u002Fuser\u002Fcontents.d\u002F\u003Cname>` so it joins the user bundle.\n5. The `COPY docker\u002Fs6-rc.d\u002F` in the Dockerfile picks it up automatically — no other changes.\n\n### Change the per-profile gateway run command\n\nEdit `S6ServiceManager._render_run_script` in `hermes_cli\u002Fservice_manager.py`. The function is also called by `hermes_cli\u002Fcontainer_boot.py::_register_service` during boot reconciliation, so it's the single source of truth. Update the corresponding assertion in `tests\u002Fhermes_cli\u002Ftest_service_manager.py::test_s6_register_creates_service_dir_and_triggers_scan`.\n\n### Run the docker test harness\n\n```sh\ndocker build -t hermes-agent-harness:latest .\nHERMES_TEST_IMAGE=hermes-agent-harness:latest scripts\u002Frun_tests.sh tests\u002Fdocker\u002F -v\n# Expect 19 passed, 0 xfailed against the s6 image\n```\n\nThe harness lives in `tests\u002Fdocker\u002F` and skips when Docker isn't available. The per-test timeout is bumped to 180s (see `tests\u002Fdocker\u002Fconftest.py`).\n\n## Common pitfalls\n\n### \"command not found\" via `docker exec`\n\n`\u002Fcommand\u002F` (where s6-overlay puts its binaries) is on PATH only for processes spawned by the supervision tree — services, cont-init.d, main-wrapper.sh. `docker exec \u003Cc> s6-svstat …` will fail with \"command not found\"; always use the absolute path `\u002Fcommand\u002Fs6-svstat`. The `hermes` binary works because the Dockerfile adds `\u002Fopt\u002Fhermes\u002F.venv\u002Fbin` to the runtime `ENV PATH`.\n\n### Profile directory ownership\n\nThe cont-init reconciler runs as hermes (`s6-setuidgid hermes` in `02-reconcile-profiles`). If a profile dir ends up root-owned (e.g. because `docker exec \u003Cc> hermes profile create …` ran as root by default), the reconciler can't read SOUL.md and fails with `PermissionError`. Mitigation: `stage2-hook.sh` chowns `$HERMES_HOME\u002Fprofiles` to hermes on **every** boot, idempotently. Don't remove that block.\n\n### Files written by `docker exec` are root-owned\n\n`docker exec` defaults to root. Either pass `--user hermes` or rely on the stage2 chown sweep next reboot. Don't write files under `$HERMES_HOME\u002Fprofiles\u002F\u003Cname>\u002F` as root manually — the next reconcile pass will sweep them but in-flight operations may hit perm errors.\n\n### Service slot exists but s6-svstat says \"s6-supervise not running\"\n\nThe service directory is on tmpfs and was wiped on container restart. Either the cont-init reconciler hasn't run yet (give it a moment after `docker restart`) or it failed. Check `docker logs \u003Cc> | grep '02-reconcile'`.\n\n### Gateway starts then immediately exits (`down (exitcode 1)` in svstat)\n\nMost likely the profile has no model or auth configured. The service slot is correct — the gateway itself is unconfigured. Run `hermes -p \u003Cprofile> setup` first. The s6 supervisor will keep restarting it; that's the desired behavior (when you fix the config, the next attempt succeeds and stays up).\n\n### Reconciler skipped a profile\n\nThe reconciler keys on the **presence of `SOUL.md`** as the \"real profile\" marker. `hermes profile create` always seeds it. If a profile dir is missing SOUL.md (stray directory, partial restore, backup-in-progress), the reconciler skips it intentionally. Add a `SOUL.md` (even empty) to opt back in.\n\n### \"Help, the container exits 143!\"\n\nCheck whether something is invoking `s6-svscanctl -t` or `\u002Frun\u002Fs6\u002Fbasedir\u002Fbin\u002Fhalt` — both cause \u002Finit to begin stage 3 shutdown but return 143 (SIGTERM) rather than the desired exit code. This was the Phase 2 architecture pivot from A to B. For container shutdown with a real exit code, you must let the CMD (main-wrapper.sh) exit normally; do **not** try to control exit from a finish script.\n\n## Related skills\n\n- `hermes-agent-dev`: General hermes-agent codebase navigation\n- `hermes-tool-quirks`: Specific Hermes-tool workarounds (sed\u002Fgrep\u002Fetc.) — load when debugging the s6 stack's interaction with hermes built-in tools.\n",{"name":5,"author":41,"license":15,"version":42,"metadata":43,"platforms":49,"description":7,"environments":50},"Hermes Agent","1.0.0",{"hermes":44},{"tags":45,"related_skills":48},[28,46,47,31,35],"s6","supervision",[34],[29],[46],"main","0d1434f89f4bc6b07e9d272accd8c1013768820f","archive\u002Fskill-md-reviewed\u002Fsha256\u002Fb9\u002Fb9b65b6d74ef9d9fee60813700bf93da7bb2f28d1f952686c82c178ebe158246",0,"https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills","https:\u002F\u002Fgithub.com\u002Fzapabob","The Open University of JAPAN　2019-now\r\n\r\nTokyo University of Science　2007-2010","2026-10-26 16:29:01",{"source":8,"skillName":5,"commands":60,"prompt":70,"zipAvailable":71},[61,64,67],{"tool":62,"command":63},"npx","npx skills add zapabob\u002FSkills --skill hermes-s6-container-supervision",{"tool":65,"command":66},"bunx","bunx skills add zapabob\u002FSkills --skill hermes-s6-container-supervision",{"tool":68,"command":69},"pnpm","pnpm dlx skills add zapabob\u002FSkills --skill hermes-s6-container-supervision","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Farchive\u002Fskill-md-reviewed\u002Fsha256\u002Fb9\u002Fb9b65b6d74ef9d9fee60813700bf93da7bb2f28d1f952686c82c178ebe158246\u002FSKILL.md",true,[],[74,107,142,169,196,213],{"skillNo":75,"name":76,"nameZh":6,"description":77,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":78,"categories":79,"keywords":91,"publishedAt":36},"01M6GJTE1GESGGF9K028Y7753E","xurl","X\u002FTwitter via xurl CLI: post, search, DM, media, v2 API.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fxurl\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[80,83,86],{"categoryNo":81,"categoryName":82,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":84,"categoryName":85,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",{"categoryNo":87,"categoryName":88,"taxonomyType":89,"taxonomyTypeDesc":90},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[76,92,93,94,95,29,96,97,98,99,100,101,102,24,33,103,51,104,105,106],"openclaw","macos","cli","github","windows","official","search","https","social-media","api","Media","twitter","social","via","post",{"skillNo":108,"name":109,"nameZh":6,"description":110,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":111,"categories":112,"keywords":124,"publishedAt":36},"01M6GJT8S1PXQ8WPXT1HSNBWH2","writing-plans","Write implementation plans: bite-sized tasks, paths, code.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fwriting-plans\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[113,116,119,120,121],{"categoryNo":114,"categoryName":115,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":117,"categoryName":118,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":84,"categoryName":85,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":87,"categoryName":88,"taxonomyType":89,"taxonomyTypeDesc":90},{"categoryNo":122,"categoryName":123,"taxonomyType":89,"taxonomyTypeDesc":90},"01JBSEEDCAT000000000000036","内容创作者",[125,126,127,128,93,129,29,130,96,131,132,133,134,135,136,137,138,139,140,141],"writing","writing plans","plans","design","review","workflow","documentation","tasks","development","code","planning","code-review","subagent","write","superpowers","paths","test-driven-development",{"skillNo":143,"name":144,"nameZh":6,"description":145,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":146,"categories":147,"keywords":151,"publishedAt":36},"01M6GJST0H5X7WHBJ3BZK0DVKH","whisper","OpenAI's general-purpose speech recognition model. Supports 99 languages, transcription, translation to English, and language identification. Six model sizes from tiny (39M params) to large (1550M params). Use for speech-to-text, podcast transcription, or multilingual audio processing. Best for robust, multilingual ASR.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fwhisper\u002Fvariants\u002Fhermes-agent-optional\u002FSKILL.md",[148],{"categoryNo":149,"categoryName":150,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[144,93,29,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168],"openai","speech","multimodal","Transformers","ASR","Audio","text","model","speech recognition","use","general","multilingual","translation","english","identification","processing","Audio Processing",{"skillNo":170,"name":171,"nameZh":6,"description":172,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":173,"categories":174,"keywords":178,"publishedAt":36},"01M6GJSMRQ8EETZTYMG72P70EA","weights-and-biases","W&B: log ML experiments, sweeps, model registry, dashboards.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fweights-and-biases\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[175],{"categoryNo":176,"categoryName":177,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000013","分析与可视化",[179,93,180,181,96,182,183,184,185,186,187,159,188,189,190,191,192,193,194,195],"weights and biases","tensorflow","experiments","pytorch","visualization","registry","real-time","tuning","dashboards","experiment","log","time","Experiment Tracking","Model Registry","Hyperparameter Tuning","WandB","Real-Time Visualization",{"skillNo":197,"name":198,"nameZh":6,"description":199,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":200,"categories":201,"keywords":203,"publishedAt":36},"01M6GJS19KDPRY112JTR326S06","watchers","Poll RSS, JSON APIs, and GitHub with watermark dedup.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fwatchers\u002Fvariants\u002Fhermes-agent-optional\u002FSKILL.md",[202],{"categoryNo":84,"categoryName":85,"taxonomyType":21,"taxonomyTypeDesc":22},[198,93,204,95,29,205,206,207,208,209,210,24,33,211,212],"terminal","automation","http","monitoring","cron","json","rss","devops","with",{"skillNo":214,"name":215,"nameZh":6,"description":216,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":217,"categories":218,"keywords":221,"publishedAt":36},"01M6GJRHJD2A6Q830SWF9W48BC","serving-llms-vllm","vLLM: high-throughput LLM serving, OpenAI API, quantization.","https:\u002F\u002Fgithub.com\u002Fzapabob\u002FSkills\u002Fblob\u002Fmain\u002Fregistry\u002Fskills\u002Fvllm\u002Fvariants\u002Fhermes-agent\u002FSKILL.md",[219,220],{"categoryNo":81,"categoryName":82,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":87,"categoryName":88,"taxonomyType":89,"taxonomyTypeDesc":90},[222,223,224,29,225,152,226,227,228,155,229,101,230,231,232,233,234,235,236,237],"llms","vLLM","serving llms vllm","research","openai api","tensor","Inference Serving","Production","llm serving","High Throughput","PagedAttention","Continuous Batching","Quantization","Tensor Parallelism","high-throughput","throughput"]