hosystem Engagements

ho-04.13 — run signal ownership

created 2026-07-06
status complete
type ho-document
project sharibako
ho 04.13
kamae 5
shape ha

Responds to a finding in ho-04.12 (; ho-04.12 is closed). D2 of that proposed that sharibako run stop forwarding SIGINT, on the premise that the child already received a terminal Ctrl-C through a shared process group. The signed-install dogfood disproved the premise and D2 was reverted. This ho decides, from scratch, who owns the child's signals — and the answer is: nobody should, because there should be no wrapper process at all once the child starts.

The finding. Foundation's Process spawns the child in its own process group, off the terminal's foreground group. A terminal Ctrl-C is delivered by the kernel only to the foreground group — i.e. to the sharibako wrapper, not the child. So the shipped 04.12-revert behavior (wrapper traps and forwards SIGINT/SIGTERM/SIGHUP with a countdown → SIGKILL escalation) is the child's only path to a terminal signal. It works, but it is a parent emulating a terminal.

Out of scope:


Phase 1 — · RATIFIED 2026-07-07

Decision 1 — Signal model: exec-replace (execve)

sharibako run replaces its own process image with the child via execve("/usr/bin/env", …) after decrypt, env-compose, and key-release. No parent exists for the child's lifetime, so the child inherits the wrapper's PID, process group, and terminal foreground — every signal and terminal behavior is native (Ctrl-C, Ctrl-, stdin reads, exit code), with zero signal-handling code in run.

Three options were weighed:

Decision 2 — Exec seam: _run returns .ready, run() execs

_run stays a pure decision function: resolve → decrypt → compose env → return RunOutcome.ready(env:command:cwd:) (replacing .ran(exitCode:)). The thin, coverage-excluded run() shim performs the execve — the same tier where Foundation.exit already lives. This keeps env composition (parent env overlaid with scope secrets, scope wins) unit-testable in-process via the .ready payload; only the untestable syscall moves behind the shim. A pure, tested helper builds the argv/envp C arrays, so the excluded surface is just chdir + execve. Do not exec inside _run — that would drop env composition to dogfood-only.

Decision 3 — Double-SIGINT concern: retired

D2's motivation — npm-class tools treat a second SIGINT as a hard abort — was an artifact of the false shared-group premise. The child is not in the foreground group, so the kernel delivers it zero tty copies; under exec-replace the child owns the foreground and receives exactly the one signal the user sends. The concern does not survive the finding and is not carried forward.

Decision 4 — Broker/handler model + output redaction: parked

Instead of a one-time env injector, Sharibako could stay alive as a broker mediating secret access over the child's lifetime (serve-on-request, audit, refresh, revoke — Vault-agent-shaped). A broker is parent-bound forever and would supersede exec-replace; it is a system- (Kamae 2) pivot, not a per-ho change. Output redaction (scrubbing known secret values from the child's stdout/stderr) is parked with it — it needs the same lifetime-parent and is foreclosed by exec-replace. Both are deferred until the whole project ships. Recorded as a GitHub issue (draft in Phase 2); parked code pinned at tag parked/run-signal-forwarder.

Decision 5 — Security tradeoff: env plaintext under a Mac-at-rest model

run puts secrets into the child's environment in plaintext for the child's lifetime — inherent to the 12-factor pattern it targets. Sharibako's boundary is a Mac at rest: FileVault protects the vault files, age key, and any temp key at rest; the injected env is protected only by macOS process isolation while the machine is unlocked. This is not total security (a hostile same-session process; no output redaction) and the stronger broker model is deliberately deferred. Accepted and recorded in SECURITY.md (Phase 2).

Context — why the parent road existed (for the record)

Not a design choice; it fell out of the tools. In Swift/Foundation, spawning a child is Process, and Process is a parent; exec-replace requires the raw POSIX execve C API. And the _run → RunOutcome.ran(exitCode:) test seam was built to assert outcomes in-process, which requires a function that returns — i.e. a parent. Signal-forwarding was never decided; it was inherited from a parent that existed for unrelated reasons. C undoes both.


Phase 2 — Execute

Branch ho-04.13 off main. The parked-code tag is already done (below), so the branch point is just current main whenever execution starts.

The exec seam (Decision 2, made concrete)

Add — replace RunCommand.spawnAndWait(...):

Delete:

Keep untouched: TempKeySignalGuard, the startupLine feedback, --dry-run, scope resolution, decrypt, env composition.

Execution gotchas

Deliverable — SECURITY.md entry (draft, ratified framing)

run injects secrets as environment variables (accepted tradeoff)

sharibako run decrypts a scope's secrets, composes them into the environment, and execs into the command. For the child's lifetime the secret values live in its process environment in plaintext.

This is a deliberate tradeoff, not an oversight. Environment variables are plaintext by nature — any tool that consumes secrets via the environment (the pattern run targets) holds them in plaintext while it runs. Sharibako's security boundary is a Mac at rest: the vault files, age key, and any temp key are protected by FileVault at rest; the injected environment is protected only by macOS process isolation while the machine is unlocked and the process is live.

We know what this does not cover: a hostile process running as the same user in an unlocked session can potentially observe the child's environment, and run does not redact secrets a child prints to its own stdout/stderr. The stronger model — a broker that never places secrets in the child's env and mediates access at runtime — is understood and deliberately deferred (see the parked issue). We have thought about it; this is the tradeoff we ship.

Deliverable — GitHub issue (draft, post after review)

Title: Park: secrets broker/handler model + output redaction for run

Context

sharibako run is a one-time environment injector: it decrypts a scope's secrets into the child's environment and (as of ho-04.13) execs into the command, replacing the wrapper. Two stronger, related capabilities were considered and deliberately parked to ship the project first.

1. Broker / handler model

Instead of dumping secrets into the child's env at startup, Sharibako stays alive as a parent and mediates secret access over the child's lifetime (serve on request, audit per access, refresh/revoke at runtime). Vault-agent-shaped. Benefits: secrets never sit in the child's environment, smaller blast radius, auditable access, runtime rotation. Cost: a lifetime-parent process — the architectural opposite of the exec-replace model run now uses, and a system-design-level pivot (it changes what run fundamentally is).

2. Output redaction

A live parent could sit in the pipe between child and terminal and replace known secret values with **** before they reach stdout/stderr — closing the "child echoes a secret into CI logs" exposure. exec-replace forecloses this (no parent in the pipe). Bundled here because it needs the same lifetime-parent the broker does.

Security tradeoff accepted in the meantime

run holds secrets in the child's env in plaintext for its lifetime, under a Mac-at-rest security model (see SECURITY.md). Known limits: same-session process exposure, no output redaction. Accepted to ship.

Parked code

The signal-forwarding parent built in ho-04.12 (SignalForwarder, ChildController/ProcessChildController, countdown/escalation) is reusable plumbing for a future broker parent. Pinned at tag parked/run-signal-forwarder.

Prerequisite

Do not start before the project ships. A broker is a Kamae-2 revisit, not a per-ho change.

Deliverable — park the code · DONE 2026-07-07

Tag parked/run-signal-forwarder created and pushed at a97b22a (the last commit with the full seam). Immutable — nothing more to do here.

git tag -a parked/run-signal-forwarder a97b22a -m "…"   # done + pushed to origin

Done means

Verification and the dogfood gate

  1. The rhythm: swift build (warnings-as-errors) → swift-format lintswiftlint --strictswift test → coverage ≥90%.
  2. Dogfood gate (signed install + real terminal — the only thing that proves C's payoff): on scripts/install.sh, run sharibako run -- <cmd> in a real terminal and confirm native behavior — Ctrl-C reaches the child directly, the exit code is the child's (128+signum on signal death), an interactive child reads stdin without stopping, and a keypress-menu dev server (Vite / Jest watch) behaves. Not done until this passes.

Phase 3 — · CLOSED 2026-07-08


Appendix — fresh-session bootstrap

To execute this ho in a new Claude Code session, load and run:

Execute ho-04.13 (Think is ratified — do NOT relitigate exec-replace). Read:
  @/hos/ho-04.13-run-signal-ownership.md   (this doc — the plan)
  @Sources/SharibakoCLI/Commands/RunCommand.swift     (_run → .ready; run() execs)
  @Sources/SharibakoCLI/Support/SignalForwarder.swift (delete)
  @Sources/SharibakoCLI/Support/RunFeedback.swift     (trim forwarding formatters)
  @Sources/SharibakoCLI/Support/TempKeySignalGuard.swift (KEEP — do not touch)
  @Tests/SharibakoCLITests/SignalForwarderTests.swift (delete)
  @Tests/SharibakoCLITests/RunCommandTests.swift      (swap .ran seam for .ready)
  @SECURITY.md                                         (add the accepted-tradeoff entry)
  @CLAUDE.md  @~/.claude/modules/languages-swift.md    (conventions)

Deliverables: the exec-replace code change; the SECURITY.md entry; post the
parked GitHub issue. (Tag parked/run-signal-forwarder already exists — do not
recreate it.) Verify with the lint/test/coverage rhythm, then the signed-install
+ real-terminal dogfood gate. Branch ho-04.13 off main. Do not sign commits/PRs.

Authored 2026-07-06; Think ratified 2026-07-07 as the forward-only response to ho-04.12's D2 dogfood finding; executed and closed 2026-07-08 (exec-replace shipped; dogfood gate caught the libdispatch signal-mask inheritance).

Rendered from the corpus, verbatim · source on GitHub →

ingested: sharibako @ 75a79d08a2b3 · ho-system @ 0f93b7fa32f7 · the glossary · the colophon