Skip to content

test(guardrails): measure the identity-domain guard on the operator's command corpus and prove parity with the local guard #9750

Description

@kyle-sexton

Parent

Refs #5945

Depends on

Needs a person: the firing-rate measurement needs the operator's recorded command corpus, and parity needs the operator's local 170-case suite; neither is in this repository.

Outcome

The identity-domain guard's firing rate on real commands is known, its false positives are regression cases,
and it matches the local guard it replaces.

Decisions

  • Epic feat(guardrails): generalize machine-local safety guards into plugin guards #5943 defaults, adopted: the guard lives in guardrails behind its dispatcher and a per-guard kill switch;
    bash under the node entry point; port behavior, not the local Python.
  • The domain map is derived from /harness-ops:machine-profile and is machine-local: it never sits in a tracked
    file, fixture or doc. Placeholders only.
  • Fail open on any internal error; every exit is 0 with the decision in stdout JSON.
  • The guard reads command text: a deterrent, not a sandbox. Declared gaps: computed paths, scripts run by name.

Done when

  • The measured firing rate on a recorded command corpus is reported on this issue or its PR, per docs/conventions/hook-precision.
  • Each false positive found is added to plugins/guardrails/hooks/block-cross-domain.test.sh as a regression case, and scripts/run-test-file.sh plugins/guardrails/hooks/block-cross-domain.test.sh exits 0.
  • The plugin guard passes behavior-equivalent test cases for the local guard it replaces (the operator retires the local copy then, outside this repo).

Activity

  1. added
    priority: mediumReal value, no hard deadline; normal backlog flow.
    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 24, 2026
  2. added theissue type on Oct 24, 2026
  3. removed
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    on Oct 26, 2026
  4. kyle-sexton commented on Oct 26, 2026

    @kyle-sexton
    MemberAuthor

    Released for re-triage under the owner's 2026-10-26 rule: a person is involved only for "1. A change in original intent/vision 2. Something comes up during implementation ... that alters intent/vision or the primary design 3. If there is a HIGH/CRITICAL security issue". None applies here, so the hold label came off and intake triage runs again now.

    To hold this issue again, add needs-human (or status: needs-decision) and name which of the three applies.

  5. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 26, 2026
  6. github-actions commented on Oct 26, 2026

    @github-actions
    Contributor

    Proposed work class: work-class: scoped
    Basis: automated intake triage; the lanes App applied this class.

    Size: Small
    Complexity: Medium
    Uncertainty: Knowable
    Flags: none
    Blocked by: none

    Understood: measure the identity-domain guard's firing rate on a recorded command corpus, add each false positive as a regression case in block-cross-domain.test.sh, and show parity with the operator's local guard.

    The corpus, the machine-local domain map and the local 170-case suite are only on the operator's host. That makes this a host-run item, which admission routes. It is not a decision for a person. The issues it depends on (#9744–#9749) no longer appear among the open issues. I found no duplicate.

    Automated triage, applied by the lanes App.

  7. 2 remaining items

  8. melodic-automation-lanes commented on Oct 26, 2026

    @melodic-automation-lanes
    Contributor

    Not admitted to the issue lane yet: blocker #9744 is not closed by a listed human or a merge; blocker #9745 is not closed by a listed human or a merge; blocker #9746 is not closed by a listed human or a merge; blocker #9747 is not closed by a listed human or a merge; blocker #9748 is not closed by a listed human or a merge; blocker #9749 is not closed by a listed human or a merge.

  9. added
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Nov 1, 2026
  10. github-actions commented on Nov 1, 2026

    @github-actions
    Contributor
    No description provided.
  11. github-actions commented on Nov 1, 2026

    @github-actions
    Contributor
    No description provided.
  12. added
    host-runNeeds an unattended run on a host; the host runner takes it, not a person or the issue lane.
    and removed
    agent-readyFully specified and briefed; eligible for autonomous pickup from the frontier.
    on Nov 1, 2026
  13. melodic-automation-lanes commented on Nov 1, 2026

    @melodic-automation-lanes
    Contributor

    The work-resolve-issue lane stopped on this issue (host-only): the run reports the work needs a host run the lane cannot make.
    It added host-run and removed agent-ready: an agent run on a host builds it, and it waits on no decision from a person.

    In the run's words:

    host-only: all three done-when items need data that lives only on the operator's host and is not in this repository. (1) Measuring the firing rate needs the operator's recorded command corpus. docs/contracts/hook-precision/guard-precision.md 'Corpus and method' names it as the local Claude Code transcripts of one operator on one host. (2) The guard decides from the identity-domain map, which the issue's Decisions keep machine-local (derived from /harness-ops:machine-profile and never in a tracked file), so a CI replay cannot reproduce real verdicts or find real false positives to add as regression cases. (3) The parity cases need the operator's local guard and its 170-case suite, which the issue says are outside this repository. The issue body says so itself: 'Needs a person: the firing-rate measurement needs the operator's recorded command corpus, and parity needs the operator's local 170-case suite; neither is in this repository.' The 2026-10-26 comment lifted the needs-human hold under the owner's rule, but it does not supply that data. The work has to run on the operator's host: replay the transcripts through plugins/guardrails/hooks/block-cross-domain.sh with the real map, report the rate per the hook-precision contract, then commit placeholder-named regression and parity cases to plugins/guardrails/hooks/block-cross-domain.test.sh.

    What the run says it did not do:

    • Firing-rate measurement on a recorded command corpus: the corpus and the machine-local domain map are only on the operator's host
    • False-positive regression cases in block-cross-domain.test.sh and a passing scripts/run-test-file.sh run: there are no measured false positives to add without the corpus
    • Parity cases against the local guard: its 170-case suite is not in this repository
    • tidy, review, verify, PR prep: skipped because no change was made

    Run: https://github.com/melodic-software/claude-code-plugins/actions/runs/41261790285

    To hand it back to the lane, remove host-run and add agent-ready again.

  14. github-actions commented on Nov 1, 2026

    @github-actions
    Contributor

    Intake triage did not triage this issue after 2 dispatches, so the sweep no longer dispatches it and labeled it needs-triage for a maintainer. Triage it by hand, and read the runs for why intake did not:

  15. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Nov 1, 2026
  16. kyle-sexton commented on Nov 2, 2026

    @kyle-sexton
    MemberAuthor

    Claim. Backlog HITL host-run worker is taking this now.

  17. kyle-sexton commented on Nov 2, 2026

    @kyle-sexton
    MemberAuthor

    Host run. Replayed the operator's desktop WSL transcript corpus through block-cross-domain.sh as shipped at 00f993925f, with no domain map, and counted the guard's logged refusals; the domain rule and the parity check could not run.

    Claude Code 2.1.316. Host: the operator's desktop WSL host.

    What ran

    • One read-only pass over the local Claude Code transcripts (25,159 transcript files, 551,256 distinct Bash and PowerShell calls by tool-use id). This session was left out.
    • Logged refusals: 7 tool results hold the text block-cross-domain: denied. On inspection all 7 are tool output that quotes the guard (source reads, greps over its file, and seeded probe scripts that exercise it), not hook refusals. Logged refusals by the guard: 0.
    • Replay: every call whose text holds both git and config, or gitconfig (a superset of the guard's identity-write prefilter), went through the hook as stdin JSON with CLAUDE_PLUGIN_DATA set to an empty throwaway directory: 25,632 calls replayed, 1 fire, 0 errors or timeouts. The corpus's --global and --system git config calls are reads (--get, --get-regexp), and the guard let all of them through.

    Result per criterion

    Criterion Status
    Firing rate on a recorded command corpus Partial. Identity-write rule only: 1 fire in 551,256 calls, 1 true positive, 0 false positives. Verdict, as the contract words its 0-to-2-fire rows: insufficient data, default unchanged. The domain rules (cross-domain path, identity positions, gh/az config directories, auth switches) were not measured. No identity-domains.json exists in the desktop's WSL plugin data or the laptop's WSL plugin data, so the guard runs only the identity-write rule there, and an invented map would make the rate meaningless.
    False positives as regression cases, test file green Nothing to add on this corpus. No false positive, so no test change and no PR.
    Parity with the local guard Blocked. The local guard and its 170-case suite are not in the desktop's WSL home, its Windows .claude profile, its dotfiles history, or the laptop's WSL .claude. The laptop's Windows profile was not checked. #5945 says the guard came from a local handoff item and its code stays outside this repository.

    Seeded probe sessions were not filtered out before the replay. The one fire came from a work worktree, not a probe, so leaving them out would not change the count.

    Decision recorded. (agent; not an owner decision) The one fire is a printf ... > .work/<dir>/<name>.gitconfig fixture write inside a gitignored directory, and it changed no live identity. It is counted as a true positive because a write to a *.gitconfig file anywhere is the guard's documented target. The suite pins that target as a deliberate deny (plugins/guardrails/hooks/block-cross-domain.test.sh:670, "a redirection to a *.gitconfig file"), and #5945 rule 6 says the local guard does the same. This follows the precedent in docs/contracts/hook-precision/guard-precision.md: a fire on a guard's documented target counts as true. Basis: the test line and the contract file above.

    Owner's step (needs the machine that holds the local guard and the real domain map)

    1. On that machine, with the plugin's map in ${CLAUDE_PLUGIN_DATA}/identity-domains.json (/guardrails:setup apply identity-domains, which builds it from the stored /harness-ops:machine-profile), replay that host's transcripts through plugins/guardrails/hooks/block-cross-domain.sh. Use stdin JSON {tool_name, cwd, tool_input:{command}} for every Bash and PowerShell call that names a mapped root, an owner directory, a marker, gh or az. Classify each fire and report aggregate counts only, per docs/contracts/hook-precision/guard-precision.md.
    2. Run the local guard's 170 cases against the plugin guard with a placeholder map that has the same shape. List every case where the two disagree.
    3. Commit each false positive and each parity case to plugins/guardrails/hooks/block-cross-domain.test.sh, using placeholder names only. Then scripts/run-test-file.sh plugins/guardrails/hooks/block-cross-domain.test.sh must exit 0. After that, retire the local copy.

    Target labels: add none; remove none.

  18. kyle-sexton commented on Nov 2, 2026

    @kyle-sexton
    MemberAuthor

    Decision recorded. Done-when 3 (behavior-equivalent cases for the local guard this replaces) is moot and dropped, under the owner's standing direction s1 ("When the evidence settles a question, act on it. Do not ask me."), re-pasted by the owner in the HITL Backlog session on 2026-11-02.

    Evidence: the 170-case local identity guard is not on the desk host, the laptop WSL host, or anywhere in the dotfiles history (searched by the HITL Backlog lane and GitHub Support, 2026-11-02). There is no local copy to match and none for the operator to retire. Done-when 1 and 2 stand.

  19. kyle-sexton commented on Nov 3, 2026

    @kyle-sexton
    MemberAuthor

    Measured firing rate of block-cross-domain on this host's command corpus: 0 false positives. Guard at bfa9d73f13, on melo-lap-001 (WSL).

    Corpus: 226,641 recorded Bash calls from 11,887 transcripts, 215,568 unique (cwd, command) pairs, 2026-09-23 to 2026-11-03. Replays A and B took the newest 30,000 pairs (2026-10-31 on). No raw command is posted here.

    Replay Pairs Denies Rate Errors
    A: no map, this host's production state 30,000 0 0% 0
    B: two-domain test map 30,000 0 0% 0
    S: map, pairs anywhere in the corpus that name the second domain 421 4 0.95% 0
    I: no map, identity-write candidates outside the newest 30,000 5,846 0 0% 0

    Denies, classified by hand: 3 true positives (a recursive search or listing of the other domain's tree from a domain cwd). 1 harmless on its face: an author.email set on a throwaway repo, which depends on a marker this test map added, and which the test file already pins as a deny. So done-when 2 has no regression case to add.

    Limits

    • The map is a test map: this host has none in production, and discover yields one domain, so the two domains and their markers were written for this replay.
    • Replay B cannot fire the path rule, because no pair in the newest 30,000 names the second tree; replay S covers that.
    • The config-directory and auth rules are not measured, because discover records no gh or az directories.

    Found

    • git config --global include.path <file> was allowed, though an include can set the identity as an includeIf does. Seen in a probe, not in the corpus. Fixed in the PR that follows.
    • A map's owners derive directories under ~/src/github.com and ~/worktrees/github.com (block-cross-domain.sh:91-92), which this host does not use, so owners adds no path here. A root still works.
    • chezmoi apply ~/.gitconfig is allowed (2 pairs). That is the sanctioned dotfiles route, so it stays allowed.

    Done-when 3 was dropped on 2026-11-02.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: securitySecurity-relevant: vulnerability, hardening, or disclosure follow-up.host-runNeeds an unattended run on a host; the host runner takes it, not a person or the issue lane.needs-triageNot yet classified. Floor until a type and one priority tier are set.priority: mediumReal value, no hard deadline; normal backlog flow.status: readyTriaged, unblocked, and fully specified; eligible to pick up.work-class: scopedA briefed fix or small feature; blast radius bounded by the brief, tests exist.

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions