docs(althing): a sixth deploy surface, and it lives outside the repo

Claude Code holds an inbound cross-session message unless the sender's
permission-mode class matches the receiver's, and holds any sender that
asserts no class. The herald is a daemon and asserts none, so on a
default-configured seat every cc poke is held at first contact and the
notice goes to whoever is watching the pane rather than to the session.

The seat still reports declared, reachable and healthy throughout — the
same failure shape as the SessionStart hook that was never deployed, where
every check was green and nothing arrived. That is what makes it worth a
surface of its own rather than a footnote.

deploy-althing.sh now reads crossSessionInbound from ~/.claude/settings.json
and reports it on every run and on --check. It deliberately does NOT set it:
that file is the operator's permission configuration, and a deploy script
that edits it is a deploy script granting itself trust.

Also records surface 5 (the per-seat route declare), which had been implicit,
and why no attestation exists for the herald to send instead — CC reads a
sender's live runtime permission mode from the session registry, which a
daemon does not appear in.
This commit is contained in:
2026-09-02 09:30:48 -07:00
parent 2575242e87
commit af5582c96d
2 changed files with 87 additions and 2 deletions
+50 -1
View File
@@ -1,4 +1,4 @@
# althing deploy — four surfaces
# althing deploy — six surfaces
**`scripts/deploy-althing.sh`** does all four. `--check` reports drift without
touching anything. This page is the why.
@@ -13,6 +13,8 @@ before anyone noticed, on 2026-09-01.
| 2 | **herald** | `systemctl --user restart althing-po-herald` | new guards live here; nothing takes effect until it restarts |
| 3 | **skill** | `scripts/sync_skill.sh``~/.agents/skills/althing/SKILL.md` | covered by its own `--check` |
| 4 | **plugin** | repo `plugin/` → marketplace dir → Claude Code cache | **two hops, neither was automated** |
| 5 | **per-seat route** | `althing-route declare` on each seat | a channel change needs a re-declare; the plugin's SessionStart hook does it for CC seats |
| 6 | **`~/.claude/settings.json`** | `crossSessionInbound: "accept"` | **outside the althing repo entirely — no version can carry it** |
## ⚠ `uv tool install .` without `--force` is a silent no-op
@@ -181,6 +183,53 @@ channel before the herald restarts goes silently pull-only. `uv tool install
(whose SessionStart hook declares the route) last, which is what keeps the
window to a couple of seconds.
## ⚠ Surface 6 — `crossSessionInbound`, and it is not in the althing repo
**A cc poke to a default-configured seat is HELD, not delivered.** Claude Code
auto-delivers an inbound cross-session message only when the sender's
permission-mode class matches the receiver's, and **a sender that asserts no
class is held**. The herald is a daemon and asserts none, deliberately. What
the operator sees instead of a delivery:
Held peer message — from an unidentified session [verified pid <herald>];
preview: «ALTHING-HERALD althing: you have mail ...» — not delivered to
Claude (1 held). The sender did not attest its permission mode and this
session bypasses prompts.
Claude Code verified the herald through `SO_PEERCRED` and then correctly
declined to let it speak, because it would not say what it was.
**The fix is one key in `~/.claude/settings.json`:**
"crossSessionInbound": "accept"
Set on nh3-dev 2026-09-02. The operator's reasoning is the part to keep: *the
herald can only reach local seats, so bypass is the correct authorization type;
when the guard was not there, that was our default posture, including the pane
poke.* A pane poke types into a session and presses Enter — bypass-level access
by any measure, and what we had been doing all along. **The socket channel is
strictly narrower than what it replaces**, so accepting here states the existing
trust boundary rather than widening it.
**There is no attestation the herald could send instead.** Four probes
established that Claude Code identifies a sender by verified pid against the
session registry and reads that session's *live runtime* permission mode; a
daemon has none and the registry has no field for one. A `from_mode` field on a
`type:"user"` frame is not consulted at all — it belongs to the control actions.
Adding it would be shipping a field nothing reads.
**Cost without the setting is smaller than it looks:** the correspondent record
is in-memory session state, so the hold is **first-contact, not per-message**
one approval per seat per session lifetime, and a long-lived pane pays it once.
**Why this surface is the dangerous one.** A seat without it is declared,
reachable and green, and its pokes go to a human instead of to the session.
That is the same shape as the SessionStart hook that was never deployed:
everything reports success, nothing arrives. `deploy-althing.sh` reads the key
and reports it on every run and on `--check` — and **deliberately never sets
it.** That file is the operator's permission configuration; a deploy script
that edits it is a deploy script granting itself trust.
## Rollback
uv tool install althing-core==3.1.2
+37 -1
View File
@@ -6,7 +6,7 @@
# scripts/deploy-althing.sh --check report drift, change nothing
#
# ─────────────────────────────────────────────────────────────────────────────
# WHY FOUR
# WHY FOUR — AND WHY THE COUNT IS NOW SIX
#
# 1. uv tool install --force . the 7 binaries (postbox, althing-listen,
# althing-route, althing-po-herald, ...)
@@ -84,6 +84,33 @@ plugin_content_drift() {
return 1
}
# ── SURFACE 6: ~/.claude/settings.json crossSessionInbound ────────────────────
#
# The only surface that is NOT in the althing repo, so a version check cannot
# reach it. Claude Code holds an inbound cross-session message unless the
# sender's permission-mode class matches yours, and a sender that asserts no
# class is held. The herald is a daemon and asserts none — deliberately — so on
# a default-configured seat a cc poke is HELD and the notice goes to whoever is
# looking at the pane instead of to the session.
#
# The failure shape is the expensive one: the seat is declared, reachable and
# green, and nothing arrives. Same family as the SessionStart hook that was
# never deployed.
#
# ⚠ REPORT ONLY, NEVER SET. This file is the operator's permission
# configuration; a deploy script that edits it is a deploy script that grants
# itself trust. Print what is there and let a human decide.
settings_inbound() {
python3 - <<'EOF' 2>/dev/null || echo "unreadable"
import json, pathlib
p = pathlib.Path.home() / ".claude/settings.json"
try:
print(json.loads(p.read_text()).get("crossSessionInbound", "UNSET"))
except Exception:
print("unreadable")
EOF
}
step "state"
say " repo $(git -C "$REPO" describe --tags --always) $(git -C "$REPO" diff --quiet && echo clean || echo DIRTY)"
say " installed tool $(uv tool list 2>/dev/null | awk '/^althing-core/{print $2}')"
@@ -91,6 +118,14 @@ say " repo plugin $(repo_plugin_version)"
say " marketplace $(market_plugin_version)"
say " CC cache $(cache_versions)"
plugin_content_drift || true
inbound="$(settings_inbound)"
say " cc inbound crossSessionInbound=$inbound (~/.claude/settings.json)"
if [[ "$inbound" != "accept" ]]; then
say " ⚠ cc pokes to a seat on this box are HELD at first contact, not delivered."
say " The seat still looks declared, reachable and healthy. Set"
say " crossSessionInbound: \"accept\" BY HAND — this script will not"
say " edit the operator's permission configuration."
fi
if (( CHECK )); then
say ""
@@ -155,5 +190,6 @@ if ! plugin_content_drift; then
say " bookkeeping. Check the diff above for whether it is load-bearing."
fi
say " herald $(systemctl --user is-active althing-po-herald)"
say " cc inbound crossSessionInbound=$(settings_inbound)"
say ""
say "⚠ RESTART Claude Code to load the new plugin hooks. A running session keeps the old ones."