Repository navigation
Description
Part of #5943
Problem
A machine that holds several identity domains (for example a work tree and a personal tree, each
with its own git author, signing key, SSH key, gh account and az login) has no plugin guard that
keeps a session rooted in one domain from acting on another. Permission rules cannot express this:
permissions.deny is not cwd-aware, so a global rule that blocks one tree's path also blocks a
session rooted in that tree, and built-in read-only Bash commands never reach the auto-mode
classifier. On origin/main no guardrails guard classifies the session or the command by domain
(plugins/guardrails/README.md, guard table).
Evidence
Verified this pass:
- No guard on main reads a domain map (
git grepoverplugins/*/hooksfor domain, identity and
cross-domain terms found nothing). plugins/harness-ops/skills/machine-profile/SKILL.mdlines 22 and 48: the machine profile already
discovers identity domains fromincludeIf "gitdir:..."entries and records each domain's tree,
git include andghconfig directory. That is a ready source for the guard's domain map.plugins/guardrails/hooks/block-exported-msys-pathconv.shrecords that a guard keyed on path shape
across every Bash command fired on 45.7% of 14,234 real commands and was rejected on that
measurement. A domain guard is path-shape matching by nature, so it inherits that risk.
Behavior of the working local guard (read for behavior only; its code does not cross into this repo):
- Classify the session
cwdinto a domain, or neutral. - Cross-domain path: deny a Bash or PowerShell command that names another domain's root in any
spelling (Windows drive path with either slash, MSYS/c/..., WSL/mnt/c/..., escaped
backslashes, any case), or a~/src/github.com/<owner>/.../~/worktrees/github.com/<owner>/...
path whose owner belongs to another domain. Reads are denied too. - Shared exception: one named directory (a cross-domain drop box) is readable and writable from
the other domain, only when every foreign path in the command is inside it and the command has
no... The exception is one-directional. - Cross-domain identity: deny a command that carries another domain's identity marker in an
identity position (git remote add/set-urlto the other org, auser.email=or--author
with the other domain's address, the other domain'sgh/azconfig dir name). gh/azconfig-dir mismatch: in a domain that needs a non-default config dir, denygh/az
without it (in the command text or the hook's environment); in the default domain deny them
with another domain's dir.--versionis exempt.- Global identity immutability (all domains, including neutral): deny a
git configwrite with
--global,--systemor--file <identity file>touchinguser.email,user.signingkey,
credential.helper,core.sshCommand,core.hooksPath,user.useConfigOnlyorincludeIf,
and any shell write to an identity file (~/.gitconfig*,*.gitconfig, a git identities
directory). Reads (--get,--list,config get) pass. - Fail-open on any internal error; every exit 0 with the decision in stdout JSON.
The source item reports 170 local test cases including WSL spellings (not re-run this pass).
Proposed approach
A new guard on the guardrails Bash|PowerShell dispatcher row, off unless a domain map is
configured, with its own kill switch and telemetry like every other guard.
Config shape (exact keys are a design decision; follow docs/conventions/plugin-option-naming
and docs/conventions/config-cascade):
domains:
<name>:
roots: [<per-OS absolute roots: Windows drive path, /mnt/<d>/..., POSIX>]
owners: [<GitHub owners mapped from ~/src|~/worktrees/github.com/<owner>/...>]
identity_markers: [<strings that identify this domain: org names, account names, config dir names>]
gh_config_dir: <optional>
az_config_dir: <optional>
identity_files: [<globs>]
shared_exceptions: [{path: <dir>, from: [<domain>], to: <domain>}]
messages: {<rule>: {windows: ..., posix: ...}}Open decisions:
- Where the domain map lives: derived from the machine profile, hand-written userConfig, or both
with the profile as default. The map is machine-specific and must never sit in a tracked repo file. - Owner plugin: guardrails, or harness-ops beside the machine profile.
- Implementation: bash in the dispatcher, or a node guard; port behavior, not the Python.
- Prefilter: a cheap substring test on configured roots and markers before any parse, per the
45.7% precedent.
Alternatives considered: permission deny rules (not cwd-aware, rejected above); a sandbox profile
(OS-level, does not express identity markers or the shared exception).
Acceptance criteria
- With no domain map configured the guard emits nothing and costs no parse.
- Each rule 2 to 6 above has deny and allow test cases, fed as stdin JSON, in Windows, MSYS and
WSL spellings, including the one-way shared exception and its..refusal. - Rule 6 applies from a neutral cwd; reads of identity config pass.
- Measured firing rate on a recorded command corpus is reported in the PR, and false positives
found are added as regression cases (docs/conventions/hook-precision). - Row cost stays within
docs/conventions/hook-budget. - README documents the config shape, the kill switch and the declared gaps.
Constraints and gotchas
- The guard reads command text; it is a deterrent, not a sandbox. Declare the gaps (computed paths,
scripts run by name). - Do not ship any real org, account or path in defaults, fixtures or docs; use placeholders.
gh/azchecks must honor the config dir set in the hook's own environment, not only in the
command text.- WSL: drive roots appear as
/mnt/<d>/..., and on Windows asC:,/c/,/cygdrive/c/.
Context
Source: local handoff item 20260928-generalize-local-guards-into-plugins.md, candidate 1. Related:
#4666 (machine profile), #1203 and #1204 (per-domain scoping in babysit config).
Metadata
Metadata
Assignees
Labels
Type
Fields
Work status
Activity
kyle-sexton commentedon Oct 24, 2026
Split by rule, adopting #5943's defaults: the guard lives in guardrails as a bash guard on the dispatcher, and the domain map is derived from
/harness-ops:machine-profileand kept in the plugin data directory, never in a tracked file. Order: the foundation first; then rules 2, 4, 5 and 6 and the setup writer in parallel; the shared exception after rule 2; the needs-human corpus and parity slice last.Each agent slice is labelled
status: readyandadmission-deferred, with Size, Complexity and Uncertainty set; admission takes unblocked slices oldest first.Posted by AI while splitting the backlog (owner decisions Q93 to Q95 and C12, 2026-10-24).
github-actions commentedon Oct 25, 2026
The triage sweep dispatched intake triage for this issue (attempt 1 of 2).
github-actions commentedon Oct 25, 2026
Proposed work class: work-class: structural
Basis: automated intake triage; the lanes App applied this class.
Size: Large
Complexity: High
Uncertainty: Knowable
Flags: none
Blocked by: none
Understood: a configurable guardrails guard that classifies a session's cwd into an identity domain. It would deny cross-domain paths, identity markers and gh/az config-dir mismatches, and refuse global git identity writes. It stays off unless a domain map is configured.
The maintainer's split comment already settles the open design decisions. The guard lives in guardrails as a bash dispatcher guard, and the map comes from the machine profile and is kept in the plugin data directory. The work is tracked in #9743 to #9750, so this issue acts as the umbrella. No likely duplicates beyond those slices.
Automated triage, applied by the lanes App.
melodic-automation-lanes commentedon Oct 25, 2026
Not admitted to the issue lane: Size large: every sub-issue is closed, but it is still open, so an item no sub-issue covers or a sub-issue's close keeps it from closing as a finished split (owner Q93). Admission takes the issue once each of those is met, when intake triages it again (
.github/actions/resolve-config/admission-rules.json).kyle-sexton commentedon Nov 3, 2026
Delivered: closing as completed.
Agent choice, not an owner answer.
block-cross-domain.test.sh:123-133and the strace census (0 process creations)...refusal: 3ba1f77a86, 7d34814927, 1f9718c1d2, 015c415c26, 53badc14e4.plugins/guardrails/README.md:641-712, from the slice commits above.block_cross_domain_enabledand the declared gaps:plugins/guardrails/README.md:97,:1357.gitandconfig, starts no process, and emits nothing for any other command. That meets the criterion's intent: no cost and no noise on a host with no map.kyle-sexton commentedon Nov 3, 2026
Closing on the verified delivery record above.