Author init scripts against the actual rc.common + procd.sh contract, not generic SysV assumptions. All mechanics below were verified against the local OpenWrt source snapshot on 2026-09-02 (see Offline Baseline); cite line numbers when they drive a decision.
Determine Task Type
| If the user wants to... | Path |
|---|---|
| Run a daemon at boot with supervision | procd service (USE_PROCD=1) → Workflow A |
| Run a one-shot boot action (state fixup, license check) | plain rc.common script with boot() override → Workflow B |
| Make an existing script start at boot / diagnose "not effective at boot" | Workflow C (troubleshooting) |
| Configure UCI defaults on first boot | Hand-off → openwrt-uci-defaults |
| Package the script into an image / control auto-enable | Hand-off → openwrt-image-build |
Prerequisites / Preflight
Run on the target device (or confirm from its image manifest):
cat /etc/openwrt_release # confirm release matches the baseline below
head -n1 /etc/rc.common # expect: #!/bin/sh
command -v procd ubus # procd runtime must exist
ls -l /etc/rc.d/ # current boot-order links (read-only inspection)
/etc/init.d/uhttpd info 2>/dev/null | head -5 # sanity: procd ubus service listing
If you cannot reach a device, produce scripts + validation commands and mark the result Build Verification Only until run on hardware. Do not claim boot-tested behavior.
Offline Baseline
Verified 2026-09-02 against local OpenWrt source snapshot (workspace upstream-refs/openwrt, Luci 128a7812f4be, packages 5caa62e0bc), convention baseline OpenWrt 25.12.5. This skill does not auto-update. Key sources: package/base-files/files/etc/rc.common, package/system/procd/files/procd.sh, package/base-files/files/etc/init.d/{boot,done}, package/base-files/files/etc/inittab.
Contracts
- Init scripts are BusyBox ash (
/bin/sh), not bash. No arrays, nolocal -n, no bashisms. - A valid init script starts with
#!/bin/sh /etc/rc.common. The kernel interprets this as/etc/rc.common <script> <action>(rc.common L7-L9, L119). enable()creates/etc/rc.d/S<START><name>only whenSTARTis set; it returns error 1 when neitherSTARTnorSTOPis set (rc.common L44-L54).USE_PROCD=1hands execution to procd via ubus;start()is redefined to callstart_service()(rc.common L121-L142). procd supervises, respawns, and reports the process.- Examples in this skill are source-verified; end-to-end boot behavior is
Pending HILuntil exercised on the target device.
Capability Boundaries and Hand-off
Read only the reference needed for the current task:
| Task | Reference |
|---|---|
| Action names, override order, enable/disable/enabled semantics, extra_command | rc.common API |
| procd_set_param parameter table, respawn defaults, triggers, PROCD_RELOAD_DELAY | procd Instance Params |
| Picking START/STOP numbers, S00-S99 anchor map, TinyNAS S98/S99 case study | Start Order |
Beyond-scope intents route to another skill:
| User intent | Route to |
|---|---|
| First-boot UCI changes, /etc/uci-defaults scripts, uHTTPd/firewall defaults | openwrt-uci-defaults |
Overlay layout, DISABLED_SERVICES, IB auto-enable internals, image reproducibility |
openwrt-image-build |
| Amlogic box flashing/recovery, U-Boot, TTL console | openwrt-serial-recovery |
| Board-specific bring-up verification on real hardware | fw-hil-testing |
Command mechanics stay in this skill even when the higher-level decision belongs elsewhere.
Workflow A — procd-managed daemon
- Choose the START number from the anchor table (Start Order). Daemons needing network/disks:
START=95..START=99is safe; setSTOPlower thanSTARTso shutdown stops them early. - Write the script — every command-line argument is one shell argument (procd.sh L28):
#!/bin/sh /etc/rc.common
# /etc/init.d/myagent — minimal procd service
USE_PROCD=1
START=99
STOP=10
start_service() {
procd_open_instance
procd_set_param command /usr/bin/myagent
procd_append_param command --config /etc/myagent.toml # one arg per call
procd_set_param respawn # defaults: 3600 5 5
procd_set_param stdout 1 # mirror stdout to syslog
procd_set_param stderr 1
procd_close_instance
}
service_triggers() {
procd_add_reload_trigger "myagent" # config change -> reload
}
- Register and start on-device:
/etc/init.d/myagent enable # needs START, else fails silently with rc=1
ls -l /etc/rc.d/S99myagent # expect the symlink
/etc/init.d/myagent restart
/etc/init.d/myagent status # expect: running
logread | grep -i procd | tail
- Reload path: define
reload_service()for a no-restart reload; with onlyservice_triggers()+reload_trigger, a config change calls/etc/init.d/<name> reload, which defaults tostart()(rc.common L165-L172).
Workflow B — one-shot boot action (no procd)
Plain rc.common scripts may override boot()/start() directly; definitions in the script replace rc.common defaults because the script is sourced last (rc.common L119). Verified pattern from the TinyNAS overlay (tinynas-boot, tinynas-license-check): no USE_PROCD, boot(){ ... }, idempotent body, logger -t <tag> for observability, defensive 2>/dev/null on restarts of optional services. See Start Order for the S98/S99 ordering rationale. Do not block forever in these functions; boot waits on them.
Workflow C — "my init script did not take effect at boot"
Check in this order; each step maps to a verified mechanism:
head -n1 /etc/init.d/<name>— must be#!/bin/sh /etc/rc.common. Scripts without this shebang are not init scripts at all.- Is
START(orSTOP) assigned? Without themenable()returns 1 and creates no link (rc.common L44-L54). ls -l /etc/rc.d/ | grep <name>and/etc/init.d/<name> enabled; echo $?— the link must exist before boot will run it.- Starts manually but not at boot → it is an enable/order problem, not a script problem. Re-check START against dependencies (Start Order).
- Starts then dies →
logread | grep procd; inspect respawn behavior (procd Instance Params); run/etc/init.d/<name> tracefor syscall tracing.
Validation Gates
sh -n /etc/init.d/<name> # syntax gate (host or device)
command -v shellcheck >/dev/null && shellcheck -s sh /etc/init.d/<name>
/etc/init.d/<name> enable && [ -L "/etc/rc.d/S$(sed -n 's/^START=//p' /etc/init.d/<name>)<name>" ]
/etc/init.d/<name> status # expect "running" after start
Build-side gate: the image overlay must NOT contain /etc/rc.d/ entries. The Image Builder auto-enables init scripts that carry the rc.common shebang during rootfs preparation; disabling is done via DISABLED_SERVICES, not by hand-creating links. Deep dive → openwrt-image-build.
Pitfalls
- Do not pass a whole command line as one quoted string to
procd_set_param command; arrays take one function argument per argv element (procd.sh L28). Do not assume space splitting. - Do not define
start()/stop()in aUSE_PROCD=1script — the procd block redefines them after sourcing your script, so your versions are silently discarded (rc.common L121-L185). Writestart_service()/stop_service()/reload_service(). - Do not call
/etc/init.d/<name> enableon a script withoutSTARTand expect success; it fails by design. - Do not hand-write
/etc/rc.d/S..links into an image overlay or commit them to the build tree; Image Builder auto-enable is the mechanism,DISABLED_SERVICESis the opt-out. - Do not assume
reloadis lighter thanrestartby default: rc.common's defaultreload()isrestart(L19-L21); only areload_service()definition changes that. - Do not tune respawn blindly:
procd_set_param respawnwith no values falls back tosystem.@service[0]UCI or 3600/5/5 (threshold/timeout/retry) (procd.sh L496-L511). A crash-looping service gives up after 5 failures — do not "fix" that by disabling respawn. - Do not sleep inside
service_triggers()to delay reload; setPROCD_RELOAD_DELAY(default 1000 ms) which procd.sh appends to every trigger (procd.sh L45, L276-L279). - Do not block in a one-shot
boot()override waiting on network or disks; the boot chain is sequential.
Official Sources
- procd init scripts guide: https://openwrt.org/docs/guide-developer/procd-init-scripts
- Init script reference: https://openwrt.org/docs/techref/initscripts
- rc.common source: https://github.com/openwrt/openwrt/blob/main/package/base-files/files/etc/rc.common
- procd.sh source: https://github.com/openwrt/openwrt/blob/main/package/system/procd/files/procd.sh
Privacy
This skill does not collect, store, or transmit user data. Init scripts may embed hostnames, network topology, or credentials in UCI/config files — treat device logs (logread), overlays, and configs as sensitive and never paste secrets into issues or examples.