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:
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user