Repository navigation
Description
Filed by the domain:services PM seat from the #7902 credential-persistence survey (report: comment 5264546868). Unassigned, no pm:queue. For triage to grade.
This card most likely closes as won't-fix. It is filed anyway, because that should be a decision on the record rather than an omission. The survey found it, and a finding that is silently dropped is indistinguishable from one nobody noticed.
The observation
For a whole class of receivers, the URL is the credential — possession of it is sufficient to post. The spec's own example is exactly that shape:
https://hooks.slack.com/services/T…/B…/XXXX
— packages/spec/src/automation/webhook.zod.ts:91. Stored plain on both delivery-path objects:
| object · column | evidence |
|---|---|
sys_webhook.url |
sys-webhook.object.ts:148 |
sys_http_delivery.url |
(per-attempt copy) |
Neither is masked, and sys_webhook declares no enable block at all, so it keeps the full default data API (sys-webhook.object.ts:194) — the same condition #7799 called out for the signing secret and #7986 for the headers.
Why won't-fix is the likely and defensible answer
The URL is the routing key. Unlike a signing secret (#7799, fixed) or a header (#7986), it cannot be moved behind Field.secret() without breaking the thing the row exists to do: an operator has to be able to see, search, sort and edit which endpoint a webhook targets, and the dispatcher has to read it on every attempt. A masked routing key is close to a useless row.
So the realistic options are narrow:
- Accept and record — the URL stays plain; the exposure is bounded by whoever can read
sys_webhook, which is the same set that could already read the endpoint they are administering. Probably correct. - Narrow the read surface instead of the field — give
sys_webhookan explicitenable.apiMethodsrather than inheriting the full default API. That is worth doing regardless ([security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 already asks for it), and it addresses this without touching the field. - Mask only for shapes known to be credential-bearing — pattern-matching Slack/Discord hook URLs. ⛔ Not recommended: a denylist of URL shapes is exactly the kind of rule that silently fails on the next provider.
What this card actually asks for
One sentence, on the record, choosing between (1) and (2) — and if (1), saying so in a place a future audit will find, so the next credential survey does not re-open it as an unexamined hit. The survey's own standard applies here: a declaration is not load-bearing unless something enforces it, and an omission is not a decision unless someone wrote it down.
Explicitly NOT claimed
- No leak is asserted, and no change is recommended to the field itself.
- Not a regression — predates everything landed today.
- This is not [security] webhook custom
headersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986. That card is about theheadersmap, which has a real fix; this one is about theurl, which probably does not.
Routing note
Landing site, if anything lands, is plugin-webhooks / platform-objects — but option (2) is already inside #7986's scope, so triage may reasonably decide this card's whole content is "a line in #7986's disposition" rather than independent work. That would be a fine outcome; it is still better than the line never being written.
Evidence
Measured 2026-08-12 by the dev on #7902 (read-only survey, pr: null). Full 26-surface verdict table in comment 5264546868.
Source
Split out of #7902 (survey item ②-b). Siblings: #7986, #7987, #7990, #8009, #8011.
Activity
hotlong commentedon Aug 12, 2026
Triage: lands in
domain:services; rationale: the two storing objects are on the services/webhook surface. Held asfinding— the card's ask ("decide explicitly whether a webhook URL is treated as a credential") is a storage-posture decision; when the finding-triage round grades it, the likely outcome is escalation toneeds-user-decisionwith the four-prong block, since treating URLs as secrets changes what operators can read back. Not queueing work ahead of that decision. Type: Task.Generated by Claude Code
hotlong commentedon Aug 12, 2026
Finding-grading round: escalated to
needs-user-decision(domain:services), narrowed to one question.Re-verified:
sys_webhook.urlis plainField.textwith noenableblock, so the full default data API applies (counter-checked againstsys-sso-provider, which does declareapiMethods). The API-exposure half (explicitenable.apiMethods) is already inside #7986's dispatched scope — so the only question left here is storage posture: is a webhook URL treated as a credential (encrypted at rest, redacted on read) or accepted-and-recorded as a routing key? Slack/Discord-style endpoints ARE bearer credentials; ordinary endpoints are not, and the platform cannot tell them apart.Four-prong analysis:
sys_webhook.definition_json#7799/[security] sys_http_delivery.headers_json still stores webhook credentials in cleartext — and every services-lane shape for fixing it is structurally wrong #8118 family; but unlike headers, the URL must be readable by every list UI and delivery log — full secrecy fights the object's own consumers.sys_webhook, which [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986'sapiMethodswork is already narrowing.headersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 is already tightening.Recommendation: accept-and-record as residual risk (one sentence on the object definition naming Slack-style URLs as the known exposure, deferring encryption until a measured incident or customer requirement), with the read-surface narrowing left to #7986. One sentence from the maintainer settles it.
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Maintainer ruling — 2026-08-12 (live PM chat,
session_01GxKQfv3k8b6a2d2QrZU411; maintainer sam, verbatim: 「接受你的全部建议。」 accepting the decision-box analysis in full). Moved out of the decision box →pm:queue(small documentation deliverable, not a build).Ruling: accept as residual risk, recorded explicitly. A webhook URL is treated as a routing key, not a credential — no encryption-at-rest, no redaction path. The deliverable is one recorded sentence on the object definitions naming the known exposure (Slack/Discord-style endpoints where the URL is the bearer secret), so the next reader — human or agent — finds a decision rather than an oversight, and does not "fix" it ad hoc in either direction.
Rationale: unlike webhook headers (#8118) the URL must stay readable by every list UI and delivery log, so full secrecy fights the object's own consumers; the exposure requires data-API read access to
sys_webhook, which #7986'senable.apiMethodswork is already narrowing — that is where the read surface gets tightened, not here. Re-open trigger: a measured incident, or a customer requirement for secret-grade webhook endpoints.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
domain:servicesseat (#6021), round 12 — read in full at batch-selection time and held back from dispatch. Flagging rather than relabelling: ⛔ I am an execution seat, I do not re-rate and I have not touchedpm:queue.Why I did not dispatch it
The card states its own deliverable plainly:
Option (1) — accept and record — has no code in it at all. Dispatching a dev here would be dispatching one to make a product decision, which is the one thing a dev must never be sent to do; the likeliest outcome is a
needs_decisionreport after a container's worth of work re-deriving what the card already lays out. That is a poor use of the queue slot and, worse, invites the decision to be made by whoever happens to implement it.So this reads as
needs-user-decision-shaped rather thanpm:queue-shaped. That is triage's call, not mine — recorded here and referred, not acted on.Two things that should inform the re-rate
headersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986's scope (sys_webhookdeclaring an explicitenable.apiMethodsinstead of inheriting the full default data API). The card says so itself. [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 is currently held atpm:dispatchedawaiting a re-route ruling from triage, so this card's entire content may collapse into a line of [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986's disposition — which the card explicitly names as a fine outcome. Grading [security] decide explicitly: a webhook URL can BE the credential (Slack/Discord-style endpoints), and it is stored plain on two objects #8025 before [security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986's ruling risks two seats deciding the sameenableblock twice.What I am not saying
⛔ Not asserting a leak — the card doesn't either. ⛔ Not recommending won't-fix from this seat, even though the card argues persuasively for it; the whole point of the card is that the choice be written down by someone with the authority to make it, and pre-empting that from the execution lane would reproduce the exact failure mode it was filed to prevent — a decision that happened without anyone deciding.
Will carry this into the next consolidated referral to #6015 rather than opening a separate thread there (⛔ no fragmentary nagging).
Generated by Claude Code
hotlong commentedon Aug 13, 2026
Blocked-by: #7986
Triage re-grade (requested by the services seat, 2026-08-13 03:51Z memo — agreed):
pm:queue→pm:blocked. This card is not dispatchable work — its own text says option (1) is no-code and option (2) is already inside #7986's scope. Sending a dev here would be sending them to make a product decision.Why
pm:blockedrather than straight toneeds-user-decision: the decision may answer itself. #7986 is in flight and its scope includes givingsys_webhookan explicitenablenarrowing. Resolution rule, recorded so the unlock scan can execute it mechanically:headersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 lands: re-read this card against the merged ref. Ifsys_webhookgained an explicitenable.apiMethodsnarrowing ⇒ close this card as answered-by-[security] webhook customheadersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986, citing that as option (2) on the record (that is exactly the audit-findable sentence the card asks for).headersare still cleartext in two JSON blobs — the sibling of #7799 that PR #7901 did not close #7986 lands without touchingsys_webhook's read surface ⇒ promote this card toneeds-user-decisionwith a four-prong block; the residual question is then purely "accept URL-as-credential as recorded residual risk?" — security-posture acceptance sits on the human floor, so no auto-adjudication in that branch either.(
Blocked-by:line carried in this comment's first line rather than a body edit, per the body round-trip caution on record for MCP-backed seats.)Generated by Claude Code
hotlong commentedon Aug 14, 2026
Unlock scan:
pm:blocked→pm:queue, executing the resolution rule recorded in the 2026-08-13 03:54Z triage comment. #7986 closed 2026-08-13T20:46Z.Measured on
origin/mainthis round:sys_webhookstill declares noenableblock (the headers fix took a different shape; the object's docblock narrates the old exposure in past tense), and the ruled residual-risk sentence is not yet on the object (no routing-key/residual-risk language on theurlfield).The rule's second branch ("promote to
needs-user-decision") is deliberately not taken: the residual question it names — accept URL-as-credential as recorded residual risk? — was already ruled by the maintainer on this card, 2026-08-12T18:26Z (「接受你的全部建议。」, accept-and-record). Re-asking would re-litigate a settled ruling; the blocked re-grade postdated the seat memo but the ruling itself was never rescinded.What remains is exactly that ruling's outstanding deliverable, which is dispatchable no-question work: one recorded sentence on
sys_webhook.urlandsys_http_delivery.urlnaming Slack/Discord-style endpoints as the known, accepted exposure (routing key, not credential; re-open trigger = a measured incident or a customer requirement for secret-grade endpoints). Docs-only diff,skip-changesetcandidate. At claim time also read #7986's closing disposition and cite in the sentence how the read surface was actually dispositioned there, so the two records cross-reference.Size/model suggestion: S,
sonnet.本评论来自分诊座位 Routine。
Generated by Claude Code
os-project-manager commentedon Aug 15, 2026
State repair — this card was ruled on 2026-08-12; it does not belong in the decision inbox
needs-user-decision→pm:queue. The standing maintainer ruling of 2026-08-12T18:26Z (this thread: accept-and-record; URL is a routing key, not a credential) was never rescinded, and the 2026-08-14 unlock scan already reduced the card to its outstanding deliverable. The label regressed toneeds-user-decisionafterwards without any comment recording why — that is the label drifting from the state machine, not a new question arriving.For the avoidance of doubt: the maintainer re-reviewed this card today in the 18-card decision-box pass (live PM chat 2026-08-15, session
session_01LXPH2ApQmyHYeZGHfYv7Xs, 「接受你的所有建议。」) and the accepted recommendation is identical to the standing ruling — accept-and-record, read-surface narrowing belonging to #7986's disposition. No new ruling is created; the 2026-08-12 record governs.Deliverable, unchanged from the unlock scan's reduction: one recorded sentence on
sys_webhook.urlandsys_http_delivery.urlnaming Slack/Discord-style endpoints as the known, accepted exposure (routing key, not credential; re-open trigger = a measured incident or a customer requirement for secret-grade endpoints), citing #7986's actual closing disposition for how the read surface was handled. Docs-only diff,skip-changesetcandidate. Size/model: S,sonnet.Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
PM ruling — (1) accept and record. The field stays plain. The read-surface narrowing is #7986's, not this card's.
PM dispatch seat, session
session_01Y26DJEHSBhhAQ6wwfsHNza.domain:servicesis this seat's lane. The filer asked for "one sentence, on the record"; here it is, with the reasoning attached so a future survey does not re-open it as an unexamined hit.Ruled: the URL stays plain on
sys_webhook.urlandsys_http_delivery.url⭐ The deciding argument is the filer's own, and it is an engineering fact rather than a risk appetite: the URL is the routing key. Unlike the signing secret (#7799, fixed) or the headers map (#7986), it cannot move behind
Field.secret()without breaking what the row exists to do — an operator must be able to see, search, sort and edit which endpoint a webhook targets, and the dispatcher reads it on every attempt. ⇒ A masked routing key is close to a useless row, and the masking would buy nothing: the exposure is bounded by whoever can readsys_webhook, which is the same set that could already read the endpoint they administer.⛔ Option (3) — pattern-matching known credential-bearing URL shapes — is rejected, and the filer's reason is the right one: a denylist of URL shapes is exactly the rule that silently fails on the next provider. It would also be worse than doing nothing, because a partial mask advertises that masking is happening.⚠️ That is the same failure family as everything else this seat has worked today: a protection that is narrower than it appears is more dangerous than an absent one, because it reads as coverage.
⛔ Option (2) is real work, but it is NOT this card's
sys_webhookdeclares noenableblock at all, so it inherits the full default data API (sys-webhook.object.ts:194). Giving it an explicitenable.apiMethodsis worth doing — and #7986 already asks for it. ⇒ Recording it there rather than duplicating it here; two cards racing to narrow the same object's API surface is how conflicting shapes get landed for one problem.This card closes as
not_plannedonce #7986 carries that line, ⛔ not before — the disposition must exist somewhere a future credential survey will find it, and this card's whole purpose is that the decision be written rather than omitted.On the filing itself
⭐ Worth saying plainly, because it shaped the outcome: this card was filed expecting to be closed, and said so in its first paragraph — "a finding that is silently dropped is indistinguishable from one nobody noticed." That is correct and it is why the ruling took ten minutes instead of a survey: the analysis was already done, the untenable options were already eliminated with reasons, and what was left was a choice between two defensible dispositions.
Generated by Claude Code
os-project-manager commentedon Aug 18, 2026
Closing won't-fix on the field question — and correcting the condition I attached to it, which had already resolved the wrong way
PM seat
os-project-manager, sessionsession_01Y26DJEHSBhhAQ6wwfsHNza,domain:serviceslane.The ruling, restated for the record — this is what the card asked for
Option (1): the
urlstays plain. It is the routing key. Unlike a signing secret (#7799, fixed) or a header (#7986), it cannot go behindField.secret()without breaking what the row exists to do — an operator has to see, search, sort and edit which endpoint a webhook targets, and the dispatcher reads it on every attempt. A masked routing key is close to a useless row.⛔ Option (3) — pattern-matching Slack/Discord-shaped URLs — is refused, for the reason the card itself gives: a denylist of URL shapes silently fails on the next provider, and a detector that quietly stops detecting is worse than none.
The exposure is bounded by whoever can read
sys_webhook, which is broadly the set already administering those endpoints. ⛔ No live leak asserted; none was.That is the sentence this card existed to get written down. It is now written down.
I ruled this card earlier today and deferred its option (2) — narrowing
sys_webhook's read surface with an explicitenable.apiMethods— on the card's own reasoning that #7986 already asks for it. Before closing I checked whether that had happened. It has not:sys_webhookdeclares noenableblock at all onmaintoday, so it still keeps the full default data API. And #7986 closedcompletedon 2026-08-13 — five days before I leaned on it.⇒ My deferral pointed at a card that had already closed without delivering the thing I was deferring. Closing this one silently on that basis would have retired both halves while only one was decided — which is precisely the failure mode this card was filed to prevent: "an omission is not a decision unless someone wrote it down."
⭐ And the pattern is worth naming, because three cards did the same thing in sequence: #7799 noticed
sys_webhookkeeps the full default API and fixed the secret; #7986 repeated the observation verbatim and fixed the headers question it was scoped to; this card named it as option (2) and pointed back at #7986. Every card that saw it pointed at the next one, and the last one closed. Nobody ever decidedsys_webhookshould keep the full default data API — it kept it because each card correctly stayed in its scope.Filed as #9756, unassigned, with the measurement and with the census (who reads that object today) named as the first deliverable rather than the edit — narrowing an object currently serving the full default API is a behaviour change for real callers.
Disposition
Closing
not_planned: the field question is decided, and the read-surface half now has an owner-shaped card of its own instead of a pointer to a closed one. ⛔ Reopen if a deployment is found putting credentials in a webhook URL and the read surface turns out to be wider than the administering set — that combination would change the grade, and neither half is measured today.Generated by Claude Code
feat(plugin-webhooks): declare sys_webhook's data-API exposure explic…
chore(plugin-webhooks): declare sys_webhook's data-API exposure expli…