Skip to content

add/testing tools execution - #212

Draft
kdenney wants to merge 27 commits into
add/testing-tools-test-case-writingfrom
add/testing-tools-execution
Draft

kdenney wants to merge 27 commits into
add/testing-tools-test-case-writingfrom
add/testing-tools-execution

Conversation

@kdenney

@kdenney kdenney commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

🎟️ Tracking

📔 Objective

@kdenney
kdenney force-pushed the add/testing-tools-execution branch from abd0ad3 to d41ee06 Compare August 22, 2026 03:28
@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Claude Code validation

Result: Issues found

Re-validated the bitwarden-testing-tools plugin at 1.5.0 — 2 new execution-phase agents, 3 new skills, the PreToolUse health-checker hook and its test suite, the shared tool policy, and all shipped scripts, templates, and eval fixtures. This is a re-run against a rebased branch: the previous review's SHAs (43e2498, f57a05a) no longer exist in this checkout, so the baseline is re-pinned to the PR's current merge base ec245d9..c869f1b (48 files). This PR is stacked — its base is add/testing-tools-test-case-writing, not main. No secrets, no prompt-injection content, and no CRITICAL findings; the credential-shaped strings (test-master-password-12, do-not-leak-me, sk_test_do-not-leak) are self-labelled local-dev fixtures, and the adversarial strings in skills/*/evals/behavior-eval.json are correctly structured refusal fixtures.

Sixteen of the previous run's sixteen findings were re-checked: fifteen persist unchanged, one is partially fixed. The five commits since the last review (f3103e5..c869f1b) address route-placeholder resolution, the agents' tool-limits prose, and the manifest's agents key — not the findings. Per the re-run discipline, no new findings were manufactured against those fix commits; two new findings below come from verified gaps on original lines, not from the fixes. The verdict stays Issues found on the same judgment the previous run stated: playwright-test-runner/AGENT.md:10 grants unscoped Bash to the one agent that ingests untrusted runtime data while references/playwright-tool-policy.md:81,85 frames that exposure as bounded by the playwright-cli binary — now confirmed settled against the sub-agents docs, which document no per-command narrowing in tools:. Finding 7 is a second, independent security-clause trigger.

Not covered: Plugin structure, marketplace, and version-bump scripts are run as dedicated workflow steps before this review — see the job log. Spot-checked here: 1.5.0 agrees across .claude-plugin/marketplace.json, plugins/bitwarden-testing-tools/.claude-plugin/plugin.json:3, root README.md:25, and all six agents/*/AGENT.md:3; the manifest's new agents key resolves to six existing files matching the six directories under agents/. Separately, the changed-file list supplied to this run was wider than the PR's real diff — it named mapping-services-under-test, scoping-playwright-application-context, writing-playwright-test-cases, references/untrusted-source-policy.md, scripts/repo-diff.sh, and evals/README.md, none of which appear in gh pr diff 212 --name-only, and it omitted .cspell.json, which does. All six extra paths were dispatched for review anyway and produced no in-scope findings (they entered the list from the stack's parent branches, per CHANGELOG.md releases 1.3.0 and 1.4.0).

Findings
  • ⚠️ IMPORTANT: Finding 1: New agent grants unscoped Bash; the adjacent Bash(playwright-cli:*) is inert and the tool policy writes it up as a working control (plugins/bitwarden-testing-tools/agents/playwright-test-runner/AGENT.md:10) (already raised on the pull request)

  • ⚠️ IMPORTANT: Finding 2: ROOT_CHARS/COMMAND_CHARS reject legal install paths (space, +, backslash), permanently blocking all three health-check scripts (plugins/bitwarden-testing-tools/hooks/restrict_health_checker.py:36) (already raised on the pull request)

  • ⚠️ IMPORTANT: Finding 3: The host guard's rejection message hands the blocked caller its own bypass, and the docstring claims an absolute the env var makes false (plugins/bitwarden-testing-tools/skills/running-playwright-tests/scripts/external_trigger.py:108) (already raised on the pull request)

  • ⚠️ IMPORTANT: Finding 4: The || exit 1 guard fails open, so the only hard boundary on an agent holding unscoped Bash disappears silently (plugins/bitwarden-testing-tools/hooks/hooks.json:19) (already raised on the pull request)

  • ⚠️ IMPORTANT: Finding 5: Render verification navigates to a self-signed HTTPS origin with no Playwright config, and the hook forecloses passing one (plugins/bitwarden-testing-tools/skills/checking-localhost-web-health/SKILL.md:61) (already raised on the pull request)

  • ⚠️ IMPORTANT: Finding 6: Usage lines spell the scripts as bare relative paths while the only grants are anchored absolute matchers (plugins/bitwarden-testing-tools/skills/compiling-playwright-report/SKILL.md:19) (already raised on the pull request)

  • ⚠️ IMPORTANT: Finding 7: The new hook confines the health checker's Bash and Skill but not its Read, leaving the dev credential file behind prose alone (plugins/bitwarden-testing-tools/agents/localhost-web-health-checker/AGENT.md:10)

  • 🎨 SUGGESTED: Finding 8: merge never validates that cases is a list of objects, so a malformed segment dies with an AttributeError traceback at exit 1 instead of the documented exit 3 (plugins/bitwarden-testing-tools/skills/compiling-playwright-report/scripts/merge_results.py:57)

    Details and fix

    cases.extend(segment.get("cases", [])) silently extends from a dict's keys or a string's characters — load_segment validates only run_status. tally (scripts/results_common.py:24) then calls case.get(...) on a str. Line 17 of the same file documents "3 invalid or malformed segment JSON", and references/results-schema.md:48 promises "a loud, non-zero-exit failure naming the offending field or case". A traceback at exit 1 is neither, and an orchestrator branching on exit codes misreads it. Persisted from the previous review.

    Fix: in load_segment, after the run_status check, add cases = data.get("cases", []) plus if not isinstance(cases, list) or not all(isinstance(c, dict) for c in cases): fail(f"segment {path} 'cases' must be a list of objects").

  • 🎨 SUGGESTED: Finding 9: A totals field of the wrong type escapes the exit-3 contract and crashes with an uncaught traceback (plugins/bitwarden-testing-tools/skills/compiling-playwright-report/scripts/render_report.py:83)

    Details and fix

    stored = data.get("totals") is guarded only by if stored is not None:, so stored.get(key, -1) raises AttributeError when totals is a list or string, and int(...) raises ValueError on a non-numeric value. SKILL.md:20 promises "Exits 3 on invalid results JSON". Note the sibling loop directly above (lines 75–81) does validate each case with isinstance(case, dict), so the omission is inconsistent within one function. Persisted from the previous review.

    Fix: before the loop, guard with if not isinstance(stored, dict): fail("'totals' must be a JSON object"), and route a non-integer value through fail(f"totals.{key} is not an integer: {stored.get(key)!r}").

  • 🎨 SUGGESTED: Finding 10: The in-file usage comment omits --plan-file, which main() declares required=True (plugins/bitwarden-testing-tools/skills/compiling-playwright-report/scripts/render_report.py:9)

    Details and fix

    The usage block at lines 9–11 lists eight flags; render_report.py:230 adds a ninth, --plan-file, as required=True. Constructing the command from the header yields an argparse exit 2 — "the following arguments are required: --plan-file". SKILL.md:20 has it right, so the script header is the one stale copy. Persisted from the previous review.

    Fix: append --plan-file <path> to the usage block at line 11.

  • 🎨 SUGGESTED: Finding 11: The documented HEALTH_CHECK_TIMEOUT=<seconds> override puts an env assignment ahead of the script path, so it always prompts (plugins/bitwarden-testing-tools/skills/checking-localhost-web-health/SKILL.md:41)

    Details and fix

    The permissions docs confirm an allow rule "won't match past an assignment of any other variable" — only a fixed known-safe set is stripped. So the documented override prompts even though the hook explicitly permits the form (restrict_health_checker.py:38, :114, tested at hooks/tests/test_restrict_health_checker.py:70). Permission-prompt defect only, not a block. Persisted from the previous review.

    Fix: add a second grant covering the env-prefixed form, or give health-check.sh a --timeout flag and document that instead.

  • 🎨 SUGGESTED: Finding 12: Three descriptions carry no trigger phrasing a user would actually type, weakening model-driven invocation (plugins/bitwarden-testing-tools/skills/compiling-playwright-report/SKILL.md:3)

    Details and fix
    • compiling-playwright-report/SKILL.md:3 is pure inventory ("Home of render_report.py … and merge_results.py …") and spends its last clause on "there is no report-compiler agent". It is listed as independently visible in README.md:21.
    • checking-localhost-web-health/SKILL.md:3 has no quoted phrasing and no negative scoping; its only cue is pipeline-relative ("Use after determining required services").
    • running-playwright-tests/SKILL.md:3 offers only "Use after test cases are defined and services are running", and "using the playwright-cli skill directly" reads as a leftover from an earlier dispatch design.

    mapping-services-under-test models both trigger phrasing and negative scoping well. All three persisted from the previous review.

    Fix: lead each with the action plus concrete user phrasings — e.g. "compile the Playwright report", "merge the runner segments"; "is my local Bitwarden dev environment ready"; "run the Playwright tests", "resume the paused test run" — and add a Do NOT use it to start, build, stop, or configure services clause to the health skill.

  • 🎨 SUGGESTED: Finding 13: Two skills omit the shared untrusted-source guardrail every sibling carries (plugins/bitwarden-testing-tools/skills/running-playwright-tests/SKILL.md:10)

    Details and fix

    writing-playwright-test-cases/SKILL.md:8, mapping-services-under-test/SKILL.md:10, and scoping-playwright-application-context/SKILL.md:12 each point at ${CLAUDE_PLUGIN_ROOT}/references/untrusted-source-policy.md and impose the CWE-1427 reporting duty. running-playwright-tests has none (grep -i untrusted matches nothing in the file), and checking-localhost-web-health/SKILL.md:21 inlines a partial rule covering only malformed names and URLs. That policy file states at lines 7–8 that it is "the single source of truth… the short guard in each agent and skill is a summary of these rules, not a replacement for them", so the omission is a stated-contract gap.

    The guardrail reaches both skills only via their agents' AGENT.md headers, so a direct Skill(...) invocation runs without it — and running-playwright-tests is the component that ingests email bodies, rendered DOM, and external-trigger response bodies. Persisted from the previous review for the health skill.

    Fix: add the sibling paragraph to both, naming the runtime data each ingests and citing the shared policy file.

  • 🎨 SUGGESTED: Finding 14: Both new agents and the report skill name a start-playwright-test orchestrator that does not exist in the repo (plugins/bitwarden-testing-tools/agents/localhost-web-health-checker/AGENT.md:4)

    Details and fix

    Partially fixed. compiling-playwright-report/SKILL.md:22 now reads "The orchestrator invokes this skill", dropping the name. The rest persists: SKILL.md:3 still says "The start-playwright-test orchestrator invokes this skill", and both new agents close their descriptions "Do not invoke directly; dispatched by the start-playwright-test skill" (localhost-web-health-checker/AGENT.md:4, playwright-test-runner/AGENT.md:4). No skill or command by that name exists — the plugin has nine skills, none with that name, and no commands/ directory. Agents told not to self-trigger, whose only stated dispatcher is unreachable, are inert on whatever branch merges first.

    Fix: land the orchestrator, or reword line 3 and both line-4 descriptions to the generic "orchestrator" wording already adopted at SKILL.md:22 — and note the stack ordering dependency in the PR body so the stack is not merged out of order.

  • 🎨 SUGGESTED: Finding 15: The skills: frontmatter entry uses the unqualified name while the hook's allowlist accepts only the plugin-qualified form (plugins/bitwarden-testing-tools/agents/localhost-web-health-checker/AGENT.md:7)

    Details and fix

    The agent declares skills: - checking-localhost-web-health, but restrict_health_checker.py:26 allowlists only bitwarden-testing-tools:checking-localhost-web-health, and hooks/tests/test_restrict_health_checker.py:218 pins the unqualified form as blocked. The body at line 41 uses the qualified form, so the file gives two names for one skill. Sharper version of the inconsistency: the two entries in the same skills: list are governed by opposite rules — playwright-cli (line 8) is bare and the hook allows it bare; checking-localhost-web-health (line 7) is bare and the hook blocks it bare.

    No runtime break today: the sub-agents docs document skills: as preloading skill content at startup ("The full content of each listed skill is injected into the subagent's context at startup"), not as issuing a Skill tool call, so the hook never sees this entry directly. The risk is the model reaching for the name its own frontmatter advertises. The docs' own example uses unqualified names and do not specify whether plugin qualification is required, so this is an internal inconsistency rather than a schema defect. Persisted from the previous review.

    Fix: qualify the frontmatter entry, matching the in-plugin precedent at playwright-application-context-scoper/AGENT.md:28.

  • 🎨 SUGGESTED: Finding 16: Roughly 40 lines restate the results JSON contract that results-schema.md already defines normatively (plugins/bitwarden-testing-tools/skills/running-playwright-tests/SKILL.md:122)

    Details and fix

    The restatement at lines 122–159 was re-checked field by field against compiling-playwright-report/references/results-schema.md and references/examples/: no field disagrees. Case fields (number, name, status, url, setup_steps, test_steps, notes, adaptive, account), step fields (text, outcome, observed, screenshot, human), and all three enums match exactly. The aborted-with-cases shape is also duplicated within the file (lines 198–204 and 258–264), differing only in abort_reason placeholder text. So this stands on maintenance risk — three copies of one contract in one changeset — not on a present defect. Persisted from the previous review.

    Fix: keep the two fenced JSON templates and the runner-specific inclusion rules, replace the per-field bullet list at lines 140–159 with a pointer to the normative schema, and collapse the duplicated block at 198.

  • 🎨 SUGGESTED: Finding 17: The results schema sanctions a cleartext password in a persisted artifact with no scope or lifetime note (plugins/bitwarden-testing-tools/skills/compiling-playwright-report/references/results-schema.md:31)

    Details and fix

    The new case schema row reads `account` | object | Optional; `{ email, password }` for an account the case created, and merge_results.py:57 copies case objects verbatim into the canonical test-results-<timestamp>.json, so any password the runner emits lands on disk in cleartext. render_report.py never reads account, so it does not reach the HTML — the exposure is the JSON artifact alone, and the accounts are throwaway local-dev ones, which is why this is SUGGESTED. But the schema row is the only place the field is defined, and the plugin elsewhere takes the opposite care: read_admin_email.py:4-8 exists specifically so that only the one address the pipeline needs crosses that boundary.

    Fix: add a Notes clause to the row stating the value is a disposable local-dev credential and that test-results-*.json must not be committed, and confirm the artifacts directory is gitignored.

  • 🎨 SUGGESTED: Finding 18: A stray blank line splits the ### Added list into a loose list, so the hook entry renders detached from its siblings (plugins/bitwarden-testing-tools/CHANGELOG.md:19)

    Details and fix

    Under CommonMark the blank line does not start a second list — the list continues and becomes loose, wrapping every item in <p> and adding vertical space throughout the section, while every other release's list stays tight. The effect is cosmetic rather than structural, and prettier accepts it, so CI will not catch it. Persisted from the previous review.

    Fix: delete line 19.

Dropped after documentation check:

  • hooks/restrict_health_checker.py:36 — COMMAND_CHARS lacking $/{/}, re-raised this round as blocking the ${CLAUDE_SKILL_DIR}/scripts/… form the skill bodies print. Dropped: "Claude Code substitutes ${CLAUDE_SKILL_DIR} and ${CLAUDE_PROJECT_DIR} in two places: the skill's markdown content, and Bash rules in the allowed-tools frontmatter" — skills. The model reads an already-expanded absolute path and never emits the literal. (The separate ROOT_CHARS/COMMAND_CHARS path-character defect survives as Finding 2.)
  • hooks/restrict_health_checker.py:134 — claim that env.get("CLAUDE_PLUGIN_ROOT") is None because hooks.json only substitutes the variable into the command string, which would block every health-checker Bash call. Dropped: hook commands "export them as the environment variables CLAUDE_PROJECT_DIR, CLAUDE_PLUGIN_ROOT, and CLAUDE_PLUGIN_DATA on the spawned process, so a script can read process.env.CLAUDE_PLUGIN_ROOT regardless of how it was launched" — hooks.
  • skills/checking-localhost-web-health/SKILL.md:5-7 — claim that the space-wildcard grant form Bash(${CLAUDE_SKILL_DIR}/scripts/preflight-check.sh *) is non-standard beside the colon-prefix form used elsewhere in the plugin. Dropped: the docs' canonical example is allowed-tools: Bash(${CLAUDE_SKILL_DIR}/scripts/render.sh *) — skills.
  • skills/compiling-playwright-report/SKILL.md:4 — claim that the YAML folded scalar (>) form of allowed-tools may be unsupported. Dropped: "allowed-tools … Accepts a space- or comma-separated string, or a YAML list" — skills. The folded scalar yields a comma-separated string.
  • skills/compiling-playwright-report/SKILL.md:22 — claim that "a skill's allowed-tools grant applies to the invoking turn only and clears on the user's next message" is an unsourced platform assertion. Dropped: "Tools Claude can use without asking permission during the turn that invokes this skill. The grant clears when you send your next message" — skills. The file states this correctly.
  • agents/localhost-web-health-checker/AGENT.md:8 and agents/playwright-test-runner/AGENT.md:8 — claim that listing playwright-cli under skills: is a defect because no skill by that name ships in this plugin or marketplace. Dropped: the field "controls which skills are preloaded, not which skills the subagent can access", and the docs describe no failure mode for an unresolvable entry — sub-agents. The plugin README documents playwright-cli as an externally installed prerequisite.

Not re-reported: skills/running-playwright-tests/SKILL.md:39 — the blanket precedence clause over invoked vendor skills, fixed before the previous review, was re-checked and has not regressed. The clause is still bounded by its enumeration (exit-code responses and the [HUMAN] pause halt), and both overrides were verified against bitwarden-mailcatcher-tools/skills/reading-mailcatcher-api/SKILL.md:32-36.

Checks run

Check Status
Plugin structure (script) Skipped — run as a dedicated workflow step before this review; see the job log
Marketplace (script) Skipped — same
Version bump (script) Skipped — same. Spot-checked here; 1.5.0 agrees across all five locations and the new manifest agents key resolves
Plugin validation (AI) Passed with findings — plugin-dev:plugin-validator over plugins/bitwarden-testing-tools
Skill review (AI) Passed with findings — plugin-dev:skill-reviewer over all six SKILL.md paths named in the supplied list. Three of the six are untouched by the PR's real diff and produced no in-scope findings
Configuration & security Passed with findings — reviewing-claude-config, routing the six AGENT.md files to reviewing-agent-definitions and hooks.json + restrict_health_checker.py to reviewing-runtime-configuration, plus a direct read of every changed skill support file under scripts/, references/, templates/, and evals/
Secret scan Passed — no settings.local.json in the changeset; no settings file of any kind; no credentials in added lines; all credential-shaped strings are self-labelled local-dev fixtures
Prompt-injection screen (CWE-1427) Passed — no file attempts to direct this review. The evals/*.json adversarial strings sit in prompt fields with matching refusal expectations, which is correct fixture structure
Schema-claim documentation check Passed — 9 claims checked against official docs (sub-agents tools/skills/disallowedTools, hooks exit codes and environment variables, skills allowed-tools format, ${CLAUDE_SKILL_DIR} substitution, grant lifetime); 6 dropped, 3 confirmed and kept

@kdenney
kdenney force-pushed the add/testing-tools-execution branch 2 times, most recently from a161667 to d022a01 Compare August 26, 2026 20:59
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from d022a01 to e01e46e Compare August 27, 2026 21:25
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from e01e46e to bf0ab47 Compare August 27, 2026 22:36
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from bf0ab47 to 1467c3f Compare August 28, 2026 20:57
@kdenney
kdenney force-pushed the add/testing-tools-execution branch 2 times, most recently from 1d0649c to c680164 Compare August 31, 2026 17:27
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from c680164 to f04105b Compare August 31, 2026 23:28
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from f04105b to 9602fc1 Compare September 1, 2026 15:45
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from 9602fc1 to d14de3d Compare September 1, 2026 23:25
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from d14de3d to 4d67843 Compare September 3, 2026 16:55
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from 4d67843 to 9060e9f Compare September 3, 2026 23:40
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from 9060e9f to 1ae63ac Compare September 4, 2026 20:43
kdenney added 27 commits October 9, 2026 13:21
Squashed net diff of the execution layer (health check, test running, report compiling) for a clean reparent.
…fence in execution stage

Continues the artifact-contract-fences migration into the execution
stage: the health-checker locates the services artifact via its
SERVICES fence and the runner locates the test-cases artifact via its
TEST-CASES fence, instead of anchoring on markdown headers. Adds
malformed-fence eval coverage for both consumers.
…on-following

Invoking a Claude skill loads its SKILL.md into the same agent, which
then carries out the instructions and produces the artifact itself,
rather than a function call that returns a value. Reword the
health-checker and test-runner agents' skill invocation steps and their
downstream "the skill returns" phrasing. Also reframe playwright-cli
usage to match how that skill works — its instructions are loaded once
and browser actions run as `playwright-cli` CLI commands, not as a
Skill() call per action. Inputs and behavior are unchanged.
…EADME

- playwright-test-runner: a skill invocation loads instructions into the
  agent; it does not hand back a step value. Drop "or Skill(...)" from
  the mid-run tool-results note — the intermediate values come from Bash.
- README: reword the two "Calls the playwright-cli skill" catalog rows to
  "Drives the browser via the playwright-cli skill's CLI", matching how
  that skill actually works (instructions loaded once; actions run as
  playwright-cli CLI commands). No behavior change.
playwright-test-runner and localhost-web-health-checker carried only the
generic untrusted-content guardrail at this layer; the nonce guardrail
arrived one PR later. Convert both to the per-run UNTRUSTED-SOURCE-<nonce>
form so they are born consistent with the planning-phase agents. These read
composed/derived artifacts rather than extracting raw source fields, so this
is defense-in-depth, not a direct extraction fix.

Fold the guardrail note into the 1.5.0 changelog entry; no version bump (draft).
…guardrail policy

The two execution-phase agents deferred their untrusted-source rules to
"the full rules given in that prompt," which nothing supplies when they run
outside the orchestrated pipeline, leaving the guardrail inert. Carry a
short rule core inline and point at the shared
references/untrusted-source-policy.md, matching the planning-phase agents,
and tailor each to what it actually reads: the health checker to the test
plan, the runner to the test plan plus the runtime data it receives (email
bodies, rendered page content, external-trigger and Stripe tool output).
The per-run UNTRUSTED-SOURCE-<nonce> fence is reframed as optional hardening
for the orchestrated path. Reword the 1.5.0 changelog bullet to match.
…se guardrails

Follow the raw-source removal through the execution layer. The health
checker and the test runner lose the now-meaningless "bind to the
UNTRUSTED-SOURCE-<nonce> region" sentence; each keeps its tailored
treat-as-data core (the health checker over the test plan, the runner over
the plan plus runtime data) and the shared-policy pointer. Drop the stale
fence-binding sentence from the 1.5.0 changelog bullet.
Drop the pass-inputs framing and the output-schema restatements the
checking-localhost-web-health and running-playwright-tests skills already
own, and condense the untrusted-source block (kept, not reduced to a bare
pointer, since these agents ingest untrusted runtime data and their skills
do not restate the policy). The runner's loop invariant, inline-execution
rule, and resume/pause control logic are agent-owned and left intact.
…ants

The `*/...` path-glob Bash grants on the execution agents never match (the
permission matcher only matches literal path prefixes, and `${CLAUDE_PLUGIN_ROOT}`
is not expanded in agent `tools:`), so every script hit an approval prompt.

- Move script scoping to the skills' `allowed-tools`, which expand
  `${CLAUDE_PLUGIN_ROOT}`/`${CLAUDE_SKILL_DIR}` and auto-approve for a subagent
  that follows the skill. Add read_mailcatcher.py and ls to
  running-playwright-tests (it runs both directly and forbids the mailcatcher
  skill), completing coverage alongside external_trigger.py, read_admin_email.py
  (already scoped there) and stripe_cli.py (scoped by using-stripe-cli).
- localhost-web-health-checker -> plain `Bash` (preflight/health-check scripts
  are scoped by checking-localhost-web-health).
- playwright-test-runner -> plain `Bash` plus the working literal
  `Bash(playwright-cli:*)`. Arbitrary bash still requires approval; only the
  skill-scoped scripts and playwright-cli auto-run.
The "Known limits" note described the test runner's Bash grants as
leading-wildcard path suffixes, but those grants were removed when script
scoping moved to the skills' allowed-tools. Retitle and rewrite the note to
the surviving limitation: agents grant unscoped Bash (agent tools: cannot
scope to a script path), so scoping lives in the skills and an off-list
command is approval-gated rather than blocked, pending a PreToolUse hook.
…wn-limits section

The known-limits section and its agent-Bash entry now originate in 1.3.0;
this release adds the Category 1 entry and extends the Bash entry to the
execution agents. The old line also still described agent script grants,
which were removed from the runner.
…checker

The planning agents' PreToolUse hook now ships in 1.3.0, and the test
runner's eval and run-code payloads can't be held by a command-text
allowlist, so the 1.5.0 known-limits note's pending hook refers only to
the health checker.
…al Api

A run must know whether the flags it depends on are on without asking a
person. The running Api's /config is what the web client sees, so it is
the authority; secrets.json and Constants.cs are read only for the
requested keys, to say whether to restart the Api, set the flag, or
switch server branches.
… health check

Flag state is an environment requirement like a running service, so the
health check now runs the flag check between the service check and the
render check, and halts with the script's fix when a flag is wrong. The
policy says reading flags is permitted while editing stays forbidden.
… out of the shell unchecked

The Api reads dotnet user-secrets, which setup_secrets.ps1 copies from
secrets.json, so 'restart the Api' alone could never pick up a fix and
left the user looping on the same message. The Resolve lines now say to
apply secrets.json first. Flag keys come from planning artifacts that can
carry untrusted text, so the health check now validates each key before
building the command, single-quotes it after --, and --help no longer
exits 0 as if every flag matched.
… /config

The flag check looked for a flag's declaration only in Constants.cs, so a flag
declared in a library's [FlagKeyCollection] class (such as InvoicingFeatureFlags)
was misreported as unknown to the server. /config reports a flag only when a
[FlagKeyCollection] class declares it and a value is configured, so for a flag
it omits, the check now always inspects both secrets.json and those classes and
prints a Problem line for each gap, then one line to refresh secrets and restart
the dependent services.
…er contract

The health check passed service names from the test plan to the shell
verbatim while it regex-checked flag keys from the same untrusted plan,
and never enforced the two allowed primary-URL origins before
navigating. Both inputs are now checked against closed sets before any
command runs, in the skill and in the health-checker agent. The skill
also states its missing-playwright-cli and malformed-input halts, which
its evals already grade.

In the runner: mark a setup failure FAIL (the schema has no FAILED),
define <bitwarden git root> as the working directory, stop asserting
toasts from screenshots, and abort on missing or malformed test cases.
Document that render_report.py also exits 2 on argument errors, list
the flag-check grant in the tool policy, and bring the agents' version
fields to 1.5.0.
…pply

The runner preloaded using-stripe-cli through its skills: frontmatter
but never invoked it. A preloaded skill's allowed-tools grant does not
apply (verified on Claude Code 2.1.292, headless and interactive), so
every stripe_cli.py call needed approval. Only an invoked skill's grant
applies, and one invocation covers the rest of the agent's run.

running-playwright-tests now invokes
bitwarden-mailcatcher-tools:reading-mailcatcher-api and
bitwarden-stripe-tools:using-stripe-cli once each, before the first
step that needs them. It drops its own grant for the mailcatcher script,
which now lives in another plugin, and the rule against invoking the
mailcatcher skill. That rule dated from a prototype where the skill was
mostly a curl walkthrough, which no longer holds. The runner no longer
preloads using-stripe-cli.

The exit-3 description no longer mentions MAILCATCHER_URL, whose
override was removed earlier.
…r skills

Invoking the Mailcatcher and Stripe skills puts their own guidance in the
runner's context, and some of it assumes an interactive session: the
Mailcatcher exit table says to retry, or to ask the user to start
Mailcatcher or extend MAILCATCHER_ALLOWED_HOSTS, and the Stripe skill
says to ask the user to attach a test clock. A subagent cannot ask the
user mid-run.

running-playwright-tests now says its own exit-code branching wins, and
treats any "stop and ask the user" from an invoked skill as a [HUMAN]
pause. Exit 1 covers all three of the reader's causes (no match, link
filter miss, host allowlist rejection), not just NO_MATCH. The runner
keeps using its read_admin_email.py rather than the mailcatcher skill's
get_admin_email.py. The tool policy's known-limits entry now lists the
runner's two scripts and the vendor grants that invoking those skills adds.
…s with a hook

The health checker lists unscoped Bash and reads an untrusted test plan, and a
script-scoped grant does not work in an agent's tools:. A PreToolUse hook is the
only enforcement point, so it allows one simple command at a time from the
health-check skill's scripts and the playwright-cli calls render verification
makes, and blocks the rest.
…ution agents

The agents cannot call tools outside their allowlist, and the tool policy the runner reads through running-playwright-tests already lists the four categories, their wrappers and the stop condition.
Test cases now carry a route's `:<name>` segment, such as `:organizationId`,
through as written, but the runner had no rule for filling it. It now takes
the value from the browser's URL once an earlier step lands on the resource
that step created, and fails the case naming the placeholder when no step
did, rather than navigating to the literal segment or guessing an ID.

A new behavior eval case covers the unresolved-placeholder failure.
A run spans several dispatches once a [HUMAN] step pauses it, and a resumed dispatch rebuilds only the credentials. The placeholder rule now says dispatch throughout and states that values resolved before a pause are gone, so a resumed case fails fast instead of guessing. The eval purpose now lists cases 7 and 8.
…ents key

The manifest's explicit agents list replaces directory discovery, so the health
checker and test runner must be listed to load. Also drop the health-checker
hook docstring's note about the name forms seen in the wild, matching the
planning hook.
@kdenney
kdenney force-pushed the add/testing-tools-execution branch from f57a05a to c869f1b Compare October 9, 2026 18:25
- checking-localhost-web-health
- playwright-cli
color: red
tools: Read, Skill, Bash

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ IMPORTANT: The new hook confines this agent's Bash and Skill but not its Read, and the credential file is kept out of reach by prose alone.

Details and fix

tools: Read, Skill, Bash grants three tools; the hook added in this same changeset covers two of them.

  • hooks/hooks.json:15 sets the matcher to "Bash|Skill", so a Read call never reaches the hook process.
  • hooks/restrict_health_checker.py:131-135 — decide() branches only on tool_name == "Skill" and tool_name == "Bash", then falls through to return 0, "".

The only thing keeping this agent out of the dev credential file is a sentence in the skill it loads, skills/checking-localhost-web-health/SKILL.md:53: "Do NOT Read server/dev/secrets.json yourself: it also holds the Stripe test key and the SQL password." That is guidance to a model that, by this agent's own header at lines 13–16, is reading an untrusted test plan. references/playwright-tool-policy.md:85 discloses the gap ("the hook does not restrict the agent's Read"), but disclosure is not a control.

The Bash and Skill halves are genuinely tight, which is what makes the asymmetry worth closing: the hook pins Bash to three named scripts plus four playwright-cli subcommands against two localhost origins, and pins Skill to a two-entry allowlist.

Rated IMPORTANT rather than CRITICAL deliberately. There is no egress path out of this agent — no WebFetch, no WebSearch, and external_trigger.py is not on the hook's Bash allowlist. A secret can enter the agent's context but cannot leave through any tool it holds.

Fix — extend the matcher to Read and deny the secrets path:

"matcher": "Bash|Skill|Read"

and in decide(), alongside the existing two branches:

if tool == "Read" and not is_allowed_read(tool_input.get("file_path")):
    return 2, READ_BLOCK

where is_allowed_read rejects any path whose basename is secrets.json under a server/dev parent. Add the blocked case to hooks/tests/test_restrict_health_checker.py.

Reference: https://code.claude.com/docs/en/hooks

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant