[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M75CF2DBT2RA0VMAB6P8Z1RF":3,"skill-same:01M75CF2DBT2RA0VMAB6P8Z1RF":57},{"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":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":37,"commitSha":38,"skillDirectory":39,"forksCount":14,"repositoryHtmlUrl":40,"repositoryHomepageUrl":6,"ownerHtmlUrl":41,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":42,"install":43,"resources":56},"01M75CF2DBT2RA0VMAB6P8Z1RF","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.","osman-jalloh-lab\u002Fmyos","osman-jalloh-lab","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F197696150?v=4","01M75CAASTD7R8BCGCDJCYFAZG","OSMAN JALLOH","01M75CAATE5G8833499GV57VW0",0,"MIT","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Fhermes-s6-container-supervision\u002FSKILL.md",[],[],"2026-11-12 13:55:43",false,"---\nname: hermes-s6-container-supervision\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.\nversion: 1.0.0\nauthor: Hermes Agent\nlicense: MIT\nmetadata:\n  hermes:\n    tags: [docker, s6, supervision, gateway, profiles]\n    related_skills: [hermes-agent, hermes-agent-dev]\n---\n\nplatforms: []\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 [\"\u002Finit\", \"\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\"]` |\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: `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.\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","platforms: []\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 [\"\u002Finit\", \"\u002Fopt\u002Fhermes\u002Fdocker\u002Fmain-wrapper.sh\"]` |\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: `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.\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":24,"license":15,"version":25,"metadata":26,"description":7},"Hermes Agent","1.0.0",{"hermes":27},{"tags":28,"related_skills":34},[29,30,31,32,33],"docker","s6","supervision","gateway","profiles",[35,36],"hermes-agent","hermes-agent-dev","main","c2b5af98407b1ad3ee4c27fc5bbb56ce09328af6","skills\u002Fhermes-s6-container-supervision","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab","2026-11-04 05:17:57",{"source":8,"skillName":5,"commands":44,"prompt":54,"zipAvailable":55},[45,48,51],{"tool":46,"command":47},"npx","npx skills add osman-jalloh-lab\u002Fmyos --skill hermes-s6-container-supervision",{"tool":49,"command":50},"bunx","bunx skills add osman-jalloh-lab\u002Fmyos --skill hermes-s6-container-supervision",{"tool":52,"command":53},"pnpm","pnpm dlx skills add osman-jalloh-lab\u002Fmyos --skill hermes-s6-container-supervision","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Fhermes-s6-container-supervision\u002FSKILL.md",true,[],[58,65,72,79,86,93],{"skillNo":59,"name":60,"nameZh":6,"description":61,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":62,"categories":63,"keywords":64,"publishedAt":19},"01M75CPVE5M6GXZ39PW5CPKCPK","xurl","X\u002FTwitter via xurl CLI: post, search, DM, media, v2 API.","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Fxurl\u002FSKILL.md",[],[],{"skillNo":66,"name":67,"nameZh":6,"description":68,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":69,"categories":70,"keywords":71,"publishedAt":19},"01M75CPQ57MF6AM46MXYSGKJ6W","writing-plans","Write implementation plans: bite-sized tasks, paths, code.","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Fwriting-plans\u002FSKILL.md",[],[],{"skillNo":73,"name":74,"nameZh":6,"description":75,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":76,"categories":77,"keywords":78,"publishedAt":19},"01M75CPFNZFXN7FT6NTF21BX5M","touchdesigner-mcp","Control a running TouchDesigner instance via twozero MCP — create operators, set parameters, wire connections, execute Python, build real-time visuals. 36 native tools.","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Ftouchdesigner-mcp\u002FSKILL.md",[],[],{"skillNo":80,"name":81,"nameZh":6,"description":82,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":83,"categories":84,"keywords":85,"publishedAt":19},"01M75CPCKM3C9SN622Z7K5DZX2","test-driven-development","TDD: enforce RED-GREEN-REFACTOR, tests before code.","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Ftest-driven-development\u002FSKILL.md",[],[],{"skillNo":87,"name":88,"nameZh":6,"description":89,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":90,"categories":91,"keywords":92,"publishedAt":19},"01M75CP9C4EWMY810CY1SW231T","teams-meeting-pipeline","Operate the Teams meeting summary pipeline via Hermes CLI — summarize meetings, inspect pipeline status, replay jobs, manage Microsoft Graph subscriptions.","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Fteams-meeting-pipeline\u002FSKILL.md",[],[],{"skillNo":94,"name":95,"nameZh":6,"description":96,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":97,"categories":98,"keywords":99,"publishedAt":19},"01M75CP4K49H7QZQ0Z7HAXKQJR","systematic-debugging","4-phase root cause debugging: understand bugs before fixing.","https:\u002F\u002Fgithub.com\u002Fosman-jalloh-lab\u002Fmyos\u002Fblob\u002Fmain\u002Fskills\u002Fsystematic-debugging\u002FSKILL.md",[],[]]