[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6GF2YV5T4C68C7DY5W2Y4MP":3,"skill-same:01M6GF2YV5T4C68C7DY5W2Y4MP":84},{"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":26,"publishedAt":47,"linkOnly":48,"markdownContent":49,"markdownBody":50,"frontMatter":51,"refName":35,"commitSha":63,"skillDirectory":64,"forksCount":65,"repositoryHtmlUrl":66,"repositoryHomepageUrl":6,"ownerHtmlUrl":67,"ownerBlogUrl":6,"ownerDescription":68,"updatedAt":69,"install":70,"resources":83},"01M6GF2YV5T4C68C7DY5W2Y4MP","hermes-s6-container-supervision",null,"Modify, debug, or extend the s6-overlay supervision tree inside the Hermes Agent Docker image — adding new services, debugging profile gateways, understanding the Architecture B main-program pattern.","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\u002Fd2\u002Fd2f9b12361281daffbee3dfc55461568684b6785fdfefa29810a8039c50a0d4c\u002FSKILL.md",[18,23],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000011","运维与部署","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46],"hermes s6 container supervision","container","hermes","tree","image","profiles","hermes-agent","pattern","main","profile","hermes agent","services","gateways","gateway","dev","debugging","debug","architecture","linux","docker","2026-11-10 10:12:30",false,"---\r\nname: hermes-s6-container-supervision\r\ndescription: Modify, debug, or extend the s6-overlay supervision tree inside the Hermes Agent Docker image — adding new services, debugging profile gateways, understanding the Architecture B main-program pattern.\r\nversion: 1.0.0\r\nauthor: Hermes Agent\r\nlicense: MIT\r\nplatforms: [linux]\r\nenvironments: [s6]\r\nmetadata:\r\n  hermes:\r\n    tags: [docker, s6, supervision, gateway, profiles]\r\n    related_skills: [hermes-agent, hermes-agent-dev]\r\n---\r\n\r\n# Hermes s6-overlay Container Supervision\r\n\r\n## When to use this skill\r\n\r\nLoad this skill when you're working on:\r\n- Adding or removing a static service in the Hermes Docker image (something that should be supervised at every container start, like the dashboard)\r\n- Diagnosing why a per-profile gateway isn't starting, restarting, or surviving `docker restart`\r\n- Understanding why the container's CMD is `\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh` and how leading-dash args reach the user's program\r\n- Modifying `cont-init.d` boot scripts (UID remap, volume seeding, profile reconciliation)\r\n- Changing the rendered run-script for per-profile gateways (Phase 4)\r\n\r\nIf you're just running the Hermes Agent and want to use Docker, see `website\u002Fdocs\u002Fuser-guide\u002Fdocker.md` instead.\r\n\r\n## Architecture at a glance\r\n\r\n```\r\n\u002Finit                                  ← PID 1 (s6-overlay v3.2.3.0)\r\n├── cont-init.d                        ← oneshot setup, runs as root\r\n│   ├── 01-hermes-setup                ← docker\u002Fstage2-hook.sh\r\n│   │   ├── UID\u002FGID remap\r\n│   │   ├── chown \u002Fopt\u002Fdata\r\n│   │   ├── chown \u002Fopt\u002Fdata\u002Fprofiles (every boot)\r\n│   │   ├── seed .env \u002F config.yaml \u002F SOUL.md\r\n│   │   └── skills_sync.py\r\n│   └── 02-reconcile-profiles          ← hermes_cli.container_boot\r\n│       ├── chown \u002Frun\u002Fservice (hermes-writable for runtime register)\r\n│       └── walk $HERMES_HOME\u002Fprofiles\u002F\u003Cname>\u002Fgateway_state.json\r\n│           → recreate \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\u002F\r\n│           → auto-start only those with prior_state == \"running\"\r\n│\r\n├── s6-rc.d (static services, in \u002Fetc\u002Fs6-overlay\u002Fs6-rc.d\u002F)\r\n│   ├── main-hermes\u002Frun                ← exec sleep infinity (no-op slot)\r\n│   └── dashboard\u002Frun                  ← if HERMES_DASHBOARD=1, runs `hermes dashboard`\r\n│\r\n├── \u002Frun\u002Fservice (s6-svscan watches; tmpfs)\r\n│   ├── gateway-coder\u002F                 ← runtime-registered per-profile\r\n│   │   ├── type        (\"longrun\")\r\n│   │   ├── run         (\"#!\u002Fcommand\u002Fwith-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run\")\r\n│   │   ├── down        (marker — present means \"registered but don't auto-start\")\r\n│   │   └── log\u002Frun     (s6-log → $HERMES_HOME\u002Flogs\u002Fgateways\u002Fcoder\u002Fcurrent)\r\n│   └── ...\r\n│\r\n└── CMD (\"main program\")               ← \u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\r\n    └── routes user args: bare exec | hermes subcommand | hermes (no args)\r\n        — exec'd by \u002Finit with stdin\u002Fstdout\u002Fstderr inherited (TTY for --tui)\r\n```\r\n\r\n## Key files\r\n\r\n| Path | Role |\r\n|---|---|\r\n| `Dockerfile` | s6-overlay install + cont-init.d wiring + `ENTRYPOINT [\"\u002Finit\", \"\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\"]` |\r\n| `docker\u002Fstage2-hook.sh` | The \"old entrypoint logic\" — UID remap, chown, seed, skills sync. Runs as cont-init.d\u002F01-hermes-setup. |\r\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. |\r\n| `docker\u002Fmain-wrapper.sh` | The container's CMD. Routes user args, drops to hermes via `s6-setuidgid`, exec's the chosen program. |\r\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. |\r\n| `docker\u002Fs6-rc.d\u002Fdashboard\u002Frun` | Conditional service — `exec sleep infinity` unless `HERMES_DASHBOARD` is truthy. |\r\n| `docker\u002Fentrypoint.sh` | Back-compat shim that `exec`s the stage2 hook. External scripts that hard-coded the old entrypoint path still work. |\r\n| `hermes_cli\u002Fservice_manager.py` | `S6ServiceManager`: `register_profile_gateway`, `unregister_profile_gateway`, `start\u002Fstop\u002Frestart\u002Fis_running`, `list_profile_gateways`. |\r\n| `hermes_cli\u002Fcontainer_boot.py` | `reconcile_profile_gateways()` — walks persistent profiles, regenerates s6 slots, emits `container-boot.log`. |\r\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. |\r\n\r\n## Why Architecture B (CMD as main program, not s6-supervised)\r\n\r\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:\r\n\r\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.\r\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\"_.\r\n\r\nSo we use the s6-overlay-native CMD pattern: `ENTRYPOINT [\"\u002Finit\", \"\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\"]`. \u002Finit prepends the wrapper 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.\r\n\r\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.\r\n\r\n## Quick recipes\r\n\r\n### Verify s6 is PID 1 in a running container\r\n\r\n```sh\r\ndocker exec \u003Cc> sh -c 'cat \u002Fproc\u002F1\u002Fcomm; readlink \u002Fproc\u002F1\u002Fexe'\r\n# Expect: s6-svscan or init \u002F \u002Fpackage\u002Fadmin\u002Fs6\u002F...\u002Fs6-svscan\r\n```\r\n\r\n### Inspect a profile gateway service\r\n\r\n```sh\r\n# \u002Fcommand\u002F isn't on docker-exec PATH — use absolute path\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svstat \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\r\n# \"up (pid …) … seconds\"            → running\r\n# \"down (exitcode N) … seconds, normally up, want up, …\" → s6 wants it up but the process keeps exiting (crash loop)\r\n# \"down … normally up, ready …\"     → user stopped it\r\n```\r\n\r\n### Bring a service up\u002Fdown manually\r\n\r\n```sh\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -u \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # up\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -d \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # down\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -t \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # SIGTERM (restart)\r\n```\r\n\r\n### Watch the cont-init reconciler log\r\n\r\n```sh\r\ndocker exec \u003Cc> tail -n 50 \u002Fopt\u002Fdata\u002Flogs\u002Fcontainer-boot.log\r\n# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started\r\n# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered\r\n```\r\n\r\n### Add a new static service\r\n\r\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`).\r\n2. Drop to hermes via `s6-setuidgid hermes` at the top of run (unless you specifically need root).\r\n3. Create empty `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Fdependencies.d\u002Fbase` so it waits for the base bundle.\r\n4. Create empty `docker\u002Fs6-rc.d\u002Fuser\u002Fcontents.d\u002F\u003Cname>` so it joins the user bundle.\r\n5. The `COPY docker\u002Fs6-rc.d\u002F` in the Dockerfile picks it up automatically — no other changes.\r\n\r\n### Change the per-profile gateway run command\r\n\r\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`.\r\n\r\n### Run the docker test harness\r\n\r\n```sh\r\ndocker build -t hermes-agent-harness:latest .\r\nHERMES_TEST_IMAGE=hermes-agent-harness:latest scripts\u002Frun_tests.sh tests\u002Fdocker\u002F -v\r\n# Expect 19 passed, 0 xfailed against the s6 image\r\n```\r\n\r\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`).\r\n\r\n## Common pitfalls\r\n\r\n### \"command not found\" via `docker exec`\r\n\r\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`.\r\n\r\n### Profile directory ownership\r\n\r\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.\r\n\r\n### Files written by `docker exec` are root-owned\r\n\r\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.\r\n\r\n### Service slot exists but s6-svstat says \"s6-supervise not running\"\r\n\r\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'`.\r\n\r\n### Gateway starts then immediately exits (`down (exitcode 1)` in svstat)\r\n\r\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).\r\n\r\n### Reconciler skipped a profile\r\n\r\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.\r\n\r\n### \"Help, the container exits 143!\"\r\n\r\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.\r\n\r\n## Related skills\r\n\r\n- `hermes-agent-dev`: General hermes-agent codebase navigation\r\n- `hermes-tool-quirks`: Specific Hermes-tool workarounds (sed\u002Fgrep\u002Fetc.) — load when debugging the s6 stack's interaction with hermes built-in tools.\r\n","# Hermes s6-overlay Container Supervision\r\n\r\n## When to use this skill\r\n\r\nLoad this skill when you're working on:\r\n- Adding or removing a static service in the Hermes Docker image (something that should be supervised at every container start, like the dashboard)\r\n- Diagnosing why a per-profile gateway isn't starting, restarting, or surviving `docker restart`\r\n- Understanding why the container's CMD is `\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh` and how leading-dash args reach the user's program\r\n- Modifying `cont-init.d` boot scripts (UID remap, volume seeding, profile reconciliation)\r\n- Changing the rendered run-script for per-profile gateways (Phase 4)\r\n\r\nIf you're just running the Hermes Agent and want to use Docker, see `website\u002Fdocs\u002Fuser-guide\u002Fdocker.md` instead.\r\n\r\n## Architecture at a glance\r\n\r\n```\r\n\u002Finit                                  ← PID 1 (s6-overlay v3.2.3.0)\r\n├── cont-init.d                        ← oneshot setup, runs as root\r\n│   ├── 01-hermes-setup                ← docker\u002Fstage2-hook.sh\r\n│   │   ├── UID\u002FGID remap\r\n│   │   ├── chown \u002Fopt\u002Fdata\r\n│   │   ├── chown \u002Fopt\u002Fdata\u002Fprofiles (every boot)\r\n│   │   ├── seed .env \u002F config.yaml \u002F SOUL.md\r\n│   │   └── skills_sync.py\r\n│   └── 02-reconcile-profiles          ← hermes_cli.container_boot\r\n│       ├── chown \u002Frun\u002Fservice (hermes-writable for runtime register)\r\n│       └── walk $HERMES_HOME\u002Fprofiles\u002F\u003Cname>\u002Fgateway_state.json\r\n│           → recreate \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\u002F\r\n│           → auto-start only those with prior_state == \"running\"\r\n│\r\n├── s6-rc.d (static services, in \u002Fetc\u002Fs6-overlay\u002Fs6-rc.d\u002F)\r\n│   ├── main-hermes\u002Frun                ← exec sleep infinity (no-op slot)\r\n│   └── dashboard\u002Frun                  ← if HERMES_DASHBOARD=1, runs `hermes dashboard`\r\n│\r\n├── \u002Frun\u002Fservice (s6-svscan watches; tmpfs)\r\n│   ├── gateway-coder\u002F                 ← runtime-registered per-profile\r\n│   │   ├── type        (\"longrun\")\r\n│   │   ├── run         (\"#!\u002Fcommand\u002Fwith-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run\")\r\n│   │   ├── down        (marker — present means \"registered but don't auto-start\")\r\n│   │   └── log\u002Frun     (s6-log → $HERMES_HOME\u002Flogs\u002Fgateways\u002Fcoder\u002Fcurrent)\r\n│   └── ...\r\n│\r\n└── CMD (\"main program\")               ← \u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\r\n    └── routes user args: bare exec | hermes subcommand | hermes (no args)\r\n        — exec'd by \u002Finit with stdin\u002Fstdout\u002Fstderr inherited (TTY for --tui)\r\n```\r\n\r\n## Key files\r\n\r\n| Path | Role |\r\n|---|---|\r\n| `Dockerfile` | s6-overlay install + cont-init.d wiring + `ENTRYPOINT [\"\u002Finit\", \"\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\"]` |\r\n| `docker\u002Fstage2-hook.sh` | The \"old entrypoint logic\" — UID remap, chown, seed, skills sync. Runs as cont-init.d\u002F01-hermes-setup. |\r\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. |\r\n| `docker\u002Fmain-wrapper.sh` | The container's CMD. Routes user args, drops to hermes via `s6-setuidgid`, exec's the chosen program. |\r\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. |\r\n| `docker\u002Fs6-rc.d\u002Fdashboard\u002Frun` | Conditional service — `exec sleep infinity` unless `HERMES_DASHBOARD` is truthy. |\r\n| `docker\u002Fentrypoint.sh` | Back-compat shim that `exec`s the stage2 hook. External scripts that hard-coded the old entrypoint path still work. |\r\n| `hermes_cli\u002Fservice_manager.py` | `S6ServiceManager`: `register_profile_gateway`, `unregister_profile_gateway`, `start\u002Fstop\u002Frestart\u002Fis_running`, `list_profile_gateways`. |\r\n| `hermes_cli\u002Fcontainer_boot.py` | `reconcile_profile_gateways()` — walks persistent profiles, regenerates s6 slots, emits `container-boot.log`. |\r\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. |\r\n\r\n## Why Architecture B (CMD as main program, not s6-supervised)\r\n\r\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:\r\n\r\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.\r\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\"_.\r\n\r\nSo we use the s6-overlay-native CMD pattern: `ENTRYPOINT [\"\u002Finit\", \"\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\"]`. \u002Finit prepends the wrapper 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.\r\n\r\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.\r\n\r\n## Quick recipes\r\n\r\n### Verify s6 is PID 1 in a running container\r\n\r\n```sh\r\ndocker exec \u003Cc> sh -c 'cat \u002Fproc\u002F1\u002Fcomm; readlink \u002Fproc\u002F1\u002Fexe'\r\n# Expect: s6-svscan or init \u002F \u002Fpackage\u002Fadmin\u002Fs6\u002F...\u002Fs6-svscan\r\n```\r\n\r\n### Inspect a profile gateway service\r\n\r\n```sh\r\n# \u002Fcommand\u002F isn't on docker-exec PATH — use absolute path\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svstat \u002Frun\u002Fservice\u002Fgateway-\u003Cname>\r\n# \"up (pid …) … seconds\"            → running\r\n# \"down (exitcode N) … seconds, normally up, want up, …\" → s6 wants it up but the process keeps exiting (crash loop)\r\n# \"down … normally up, ready …\"     → user stopped it\r\n```\r\n\r\n### Bring a service up\u002Fdown manually\r\n\r\n```sh\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -u \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # up\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -d \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # down\r\ndocker exec \u003Cc> \u002Fcommand\u002Fs6-svc -t \u002Frun\u002Fservice\u002Fgateway-\u003Cname>   # SIGTERM (restart)\r\n```\r\n\r\n### Watch the cont-init reconciler log\r\n\r\n```sh\r\ndocker exec \u003Cc> tail -n 50 \u002Fopt\u002Fdata\u002Flogs\u002Fcontainer-boot.log\r\n# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started\r\n# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered\r\n```\r\n\r\n### Add a new static service\r\n\r\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`).\r\n2. Drop to hermes via `s6-setuidgid hermes` at the top of run (unless you specifically need root).\r\n3. Create empty `docker\u002Fs6-rc.d\u002F\u003Cname>\u002Fdependencies.d\u002Fbase` so it waits for the base bundle.\r\n4. Create empty `docker\u002Fs6-rc.d\u002Fuser\u002Fcontents.d\u002F\u003Cname>` so it joins the user bundle.\r\n5. The `COPY docker\u002Fs6-rc.d\u002F` in the Dockerfile picks it up automatically — no other changes.\r\n\r\n### Change the per-profile gateway run command\r\n\r\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`.\r\n\r\n### Run the docker test harness\r\n\r\n```sh\r\ndocker build -t hermes-agent-harness:latest .\r\nHERMES_TEST_IMAGE=hermes-agent-harness:latest scripts\u002Frun_tests.sh tests\u002Fdocker\u002F -v\r\n# Expect 19 passed, 0 xfailed against the s6 image\r\n```\r\n\r\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`).\r\n\r\n## Common pitfalls\r\n\r\n### \"command not found\" via `docker exec`\r\n\r\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`.\r\n\r\n### Profile directory ownership\r\n\r\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.\r\n\r\n### Files written by `docker exec` are root-owned\r\n\r\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.\r\n\r\n### Service slot exists but s6-svstat says \"s6-supervise not running\"\r\n\r\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'`.\r\n\r\n### Gateway starts then immediately exits (`down (exitcode 1)` in svstat)\r\n\r\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).\r\n\r\n### Reconciler skipped a profile\r\n\r\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.\r\n\r\n### \"Help, the container exits 143!\"\r\n\r\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.\r\n\r\n## Related skills\r\n\r\n- `hermes-agent-dev`: General hermes-agent codebase navigation\r\n- `hermes-tool-quirks`: Specific Hermes-tool workarounds (sed\u002Fgrep\u002Fetc.) — load when debugging the s6 stack's interaction with hermes built-in tools.\r\n",{"name":5,"author":52,"license":15,"version":53,"metadata":54,"platforms":61,"description":7,"environments":62},"Hermes Agent","1.0.0",{"hermes":55},{"tags":56,"related_skills":59},[46,57,58,40,32],"s6","supervision",[33,60],"hermes-agent-dev",[45],[57],"0d1434f89f4bc6b07e9d272accd8c1013768820f","archive\u002Fskill-md-reviewed\u002Fsha256\u002Fd2\u002Fd2f9b12361281daffbee3dfc55461568684b6785fdfefa29810a8039c50a0d4c",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":71,"prompt":81,"zipAvailable":82},[72,75,78],{"tool":73,"command":74},"npx","npx skills add zapabob\u002FSkills --skill hermes-s6-container-supervision",{"tool":76,"command":77},"bunx","bunx skills add zapabob\u002FSkills --skill hermes-s6-container-supervision",{"tool":79,"command":80},"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\u002Fd2\u002Fd2f9b12361281daffbee3dfc55461568684b6785fdfefa29810a8039c50a0d4c\u002FSKILL.md",true,[],[85,116,151,178,205,222],{"skillNo":86,"name":87,"nameZh":6,"description":88,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":89,"categories":90,"keywords":100,"publishedAt":47},"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",[91,94,95],{"categoryNo":92,"categoryName":93,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000010","后端与接口",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":96,"categoryName":97,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000030","开发工程师","PROFESSION","适用职业",[87,101,102,103,104,45,105,106,107,108,109,110,111,29,37,112,35,113,114,115],"openclaw","macos","cli","github","windows","official","search","https","social-media","api","Media","twitter","social","via","post",{"skillNo":117,"name":118,"nameZh":6,"description":119,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":120,"categories":121,"keywords":133,"publishedAt":47},"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",[122,125,128,129,130],{"categoryNo":123,"categoryName":124,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":126,"categoryName":127,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":96,"categoryName":97,"taxonomyType":98,"taxonomyTypeDesc":99},{"categoryNo":131,"categoryName":132,"taxonomyType":98,"taxonomyTypeDesc":99},"01JBSEEDCAT000000000000036","内容创作者",[134,135,136,137,102,138,45,139,105,140,141,142,143,144,145,146,147,148,149,150],"writing","writing plans","plans","design","review","workflow","documentation","tasks","development","code","planning","code-review","subagent","write","superpowers","paths","test-driven-development",{"skillNo":152,"name":153,"nameZh":6,"description":154,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":155,"categories":156,"keywords":160,"publishedAt":47},"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",[157],{"categoryNo":158,"categoryName":159,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000016","图像与多媒体",[153,102,45,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177],"openai","speech","multimodal","Transformers","ASR","Audio","text","model","speech recognition","use","general","multilingual","translation","english","identification","processing","Audio Processing",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":187,"publishedAt":47},"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",[184],{"categoryNo":185,"categoryName":186,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000013","分析与可视化",[188,102,189,190,105,191,192,193,194,195,196,168,197,198,199,200,201,202,203,204],"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":206,"name":207,"nameZh":6,"description":208,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":209,"categories":210,"keywords":212,"publishedAt":47},"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",[211],{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},[207,102,213,104,45,214,215,216,217,218,219,29,37,220,221],"terminal","automation","http","monitoring","cron","json","rss","devops","with",{"skillNo":223,"name":224,"nameZh":6,"description":225,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":226,"categories":227,"keywords":230,"publishedAt":47},"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",[228,229],{"categoryNo":92,"categoryName":93,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":96,"categoryName":97,"taxonomyType":98,"taxonomyTypeDesc":99},[231,232,233,45,234,161,235,236,237,164,238,110,239,240,241,242,243,244,245,246],"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"]