Please report security problems privately, not in a public issue or pull request. Use GitHub's private vulnerability reporting: open this form, or click Report a vulnerability on the repository's security tab.
Include the mendwright version, the browser library and provider you used, what happened, and the smallest reproduction you have. Remove page content, keys and tokens from anything you attach. Redaction lists what mendwright itself doesn't remove.
I'll reply on the report itself, and we'll agree on the fix and a disclosure date there.
mendwright treats the model's output as untrusted. The model sees a redacted copy of the page, and a hostile page can try to steer it. A proposal is only a string. mendwright uses it after the local checks below pass, and the model never touches the page or the store.
mendwright check and heal.drift() never act on the page, so a hostile page can't get them to click or type. The one exception is the consent preset with mendwright check --cookies, which clicks a banner's button under the rules of the cookies= preset. They call no model, and a heal they find is only reported, never saved.
mendwright check loads addresses only on the sites you name with --origin or --base-url, over http or https. The store never chooses a host, so a store from a cache or a pull request can't send the browser, and the session of a storage state file, to another site, a local file or a cloud metadata address. With --base-url, an address is built from a key's lastPage only when it is a path as mendwright saves one, never from one that holds user info, a port, a query or anything else. It doesn't load a page whose path reads as an action (sign out, unsubscribe, cancel) unless --load-page names it, writes nothing but its --out file, and never writes the storage state file back. The site's own JavaScript runs in the page, and a GET can still change state on a badly built site, so point it at a test site and a test account. A site that rotates refresh tokens can end the session in a storage state file once check uses it: give check its own sign-in.
Every proposed XPath goes through client.apply(), which runs two checks.
validate() in mendwright/validation.py evaluates the XPath against the captured HTML with lxml. It rejects the proposal unless:
- it isn't a pure absolute-index path such as
/html[1]/body[1]/div[3]/input[1]; - it matches exactly one element;
- that element has the key's expected role (ARIA names such as
textboxorswitchare mapped to the element roles they mean); - for a key marked destructive, the element's text is one of the expected texts. The coordinator refuses destructive keys before this point, so this rule only matters when you call
validate()yourself.
live_match() in mendwright/gate.py then checks the XPath on the current page through the driver.
It must match exactly one element, that element must be visible unless its role is checkbox, and it must still have the key's role. For a destructive key, its text must also be one of the expected texts. Any driver error counts as no match.
mendwright.open() and mendwright.aio.open() always pass a driver. A HealCoordinator you build yourself runs this check only when you pass driver=.
For the key of a heal.page.get_by_role() call (with mendwright.open(page, heal_get_by=True)), Playwright must also give the element the call's ARIA role, and the call's level= when it has one, by its own matcher on the live page: mendwright's role is mostly the tag, so a <button role="tab"> that is no longer a tab would pass it. A driver that can't answer refuses the heal.
apply() also rejects a proposal that the store holds as quarantined or superseded, or that reaches an element the key has quarantined, even through a different XPath. It never stores a selector that isn't XPath.
A proposal's frame hint is ignored; frame context is never taken from model output.
If the first proposal fails, the coordinator asks once more with the rejection reason. After that it raises AttentionRequired.
Other limits on a heal:
guard: a callable passed tomendwright.open(guard=...). While it returnsFalse, nothing is healed and no model is called.- Rate limit: direct providers get 3 heal attempts per key and 60 in total per hour by default (
rate_per_key_per_hour,rate_global_per_hour). Every attempt that reaches the provider uses a slot, including rejected and dry-run ones, and makes up to 2 model calls. The per-key limit counts the attempts of every process on the machine that uses the store file, and of every machine on a remote store; the global limit is per store file within one process. Workers that wait for another worker's heal spend none. When it's reached, the heal is declined. - Confirmation: a new heal counts as confirmed only after
expect=on the action passes,.confirm(True),Healer.confirm_all(True), or the pytest plugin sees the test pass. If the action fails in the browser, orexpect=returns false, the healed selector is quarantined and isn't tried again. MENDWRIGHT_DISABLE=1: selectors still resolve, but nothing is healed and no provider is called.get_by_*calls: withheal_get_by=True, aheal.page.get_by_*()call is healed from its fingerprint only. No provider, gateway or local model is asked for one, so no page HTML, inventory or key leaves the machine for a call. A call with no fingerprint in the store is not healed, and a call that matches several elements or one hidden element is never healed or answered by a saved heal.get_by_*calls withheal_get_by="model"(opt-in, alsoMENDWRIGHT_HEAL_GET_BY=model): when the fingerprint doesn't heal a call, the configured provider is asked, as for any key, so a hosted model or a gateway gets the redacted page and inventory of the call's page, the call itself as the key and the broken selector, and what the call says (its role and name, text, label, placeholder, alt text, title or test id) as the intent. The fingerprint still goes only toLocalModelProvider. Every check above applies, and the hourly rate limit counts each such heal. A call that never matched is sent only when it is aget_by_role()call, whose role the pick is checked against; Playwright must give the pick that ARIA role, and the page guard can't decline such a call, since no page was recorded for it. A call that matches several elements or one hidden element is never sent.
A key that is destructive is never healed, and no model is called for it. A key is destructive only when you mark it:
- the seed file marks it, or it is in
mendwright.open(destructive_keys=...). Nothing at run time can clear these. heal.locator(..., destructive=True)marks it. This is saved in the override file, anddestructive=Falseclears it.
The key of a heal.page.get_by_*() call is the call, as mendwright.get_by_key() writes it. The same call written in another spelling, such as get_by_role('button', name='Pay'), marks it too. A string that isn't a call, such as page.get_by_role('button', name='Pay'), is a key of its own and marks nothing a call uses. For a call written on an object like that in destructive_keys= or a seed file, mendwright logs a WARNING that gives the call's key.
mendwright doesn't guess which controls are destructive from the key, text= or the page. A key you don't mark can be healed onto any element that has its role, including a button that pays or deletes.
A heal saved for a key before it was marked is quarantined, not acted on.
mendwright.open(destructive_keys=...) is not saved in the store, so mendwright fix can't see it there. Pass the same keys to mendwright fix --destructive-key, and to the GitHub Action's destructive-keys input, so a heal saved before the key was marked is never written into your code.
With the low-level HealCoordinator, the flag comes from meta['destructive'], which store.meta(key) fills from the seed file, destructive_keys and saved flags.
This is what a refused key looks like, from a run with a stub provider:
mendwright.exceptions.AttentionRequired: No matching element for: pay (tried 1 candidate(s)).
-> 'pay' is marked as a destructive control, and mendwright never auto-heals those, because a validated but wrong selector here could act on a live account. Fix the selector in your code or seed file by hand.
The provider received 0 calls.
Redaction is best-effort. Visible text and most attribute values reach the model provider.
build_bundle() in mendwright/capture.py prepares the page in three steps:
redact(). It removes quotedvalue="..."andvalue='...'attributes. It replaces caller-supplied row secrets with[redacted]: the values inrow['secrets']androw['proxy'], and every string underrow['profile'], in plain and HTML-escaped form. It replaces absolutehttp(s)://URLs with[URL], and runs of 12 to 19 digits with[redacted number].clean_html(). It removes<script>,<style>,<noscript>,<template>, comments and processing instructions, empties<svg>, removes<meta>,<link>and<base>from<head>, and collapses whitespace. If lxml can't parse the page, the text from step 1 is used as is.redact()again. lxml writes every attribute with quotes, so an unquotedvalue=from the original page is removed here.
The inventory sent with the page never contains input values: the drivers don't read them, every string gets the same redaction, and keys named value are dropped.
The broken selectors and the expected text in the bundle get the same redaction as the page.
The key's saved fingerprint (the tag, stable attributes, text and label of the element the key last matched) goes into the bundle only for a provider that sets wants_fingerprint = True. Of the built-in providers only LocalModelProvider does, and it talks to Ollama on your machine. The fingerprint gets the same redaction.
The page URL isn't part of the prompt, so direct providers never send it. GatewayProvider posts the whole bundle, in which an http(s) page URL is replaced with [URL]. A URL with another scheme, such as file://, is sent as it is.
The local checks use the full, unredacted page; none of it is sent anywhere.
redact() on its own does step 1 only. It misses unquoted value= attributes and keeps scripts and <meta> tags.
This is real input and output for one element, with row={'profile': {'name': {'first': 'Jane'}}}:
in: <span aria-label="Jane Doe" data-user="u42">jane@example.com 555-1234 4111111111111111 https://x.com/a?b=1</span>
out: <span aria-label="[redacted] Doe" data-user="u42">jane@example.com 555-1234 [redacted number] [URL]</span>
What isn't removed:
- visible text, including
<textarea>andcontenteditablecontent; aria-label,title,placeholder,altanddata-*values;- email addresses, and numbers shorter than 12 digits;
- relative links with tokens in the query, such as
/reset?token=abc123; - anything in a page lxml can't parse, apart from step 1.
mendwright.open() doesn't take a row, so the facade has no row secrets to replace. The attribute, URL and number rules still apply. To redact specific values, pass row= to resolve_with_heal() on the low-level API.
Ways to send less:
- Use a model served by Ollama on your own machine (
OLLAMA_MODEL, or mendwright's own selector model withMENDWRIGHT_LOCAL_MODEL), so the page doesn't leave it. The selector model is sent a short list of candidate elements instead of the page. - On the low-level API, give
HealCoordinatoranhtml_providerthat returns only the part of the page the key lives in. - Set
MENDWRIGHT_DISABLE=1where no page content may leave the machine. Dry run (MENDWRIGHT_DRY_RUN=1) still calls the provider.
When a provider call fails, provider.last_error and the AttentionRequired message say why.
That text is built in mendwright/providers/_errors.py. It never includes the request body, and it masks:
- the provider's own API key;
- key-shaped tokens:
sk-,rk-,pk-,gsk-andxai-prefixes, GoogleAIzakeys, AWSAKIAandASIAkey ids, JWTs, and hex strings of 32 or more characters; BearerandBasicvalues of 16 or more characters;- echoed
api-key,x-goog-api-key,authorization,auth-token,access-token,secretandheal-keyvalues; - the query parameters
key,api_key,token,access_token,sig,signatureandx-amz-*; - URL user info and query strings, when the URL is shown as the request target.
Vendor messages are cut to 200 characters. For example:
in: Incorrect API key provided: sk-proj-abc123def456ghi. Authorization: Bearer abcdefghijklmnopqrstuvwxyz0123 see https://gw.example/heal?key=s3cr3tvalue&x=1
out: Incorrect API key provided: [redacted] Authorization: Bearer [redacted] see https://gw.example/heal?key=[redacted]&x=1
mendwright doctor adds its own masking. It replaces the value of any environment variable whose name contains KEY, TOKEN, SECRET, PASSWORD, PASS, AUTH, CREDENTIAL or WEBHOOK with *** wherever it appears in error text and in the values it shows. It shows MENDWRIGHT_GATEWAY as scheme and host only, MENDWRIGHT_WEBHOOK_URL as mendwright notify names the webhook (scheme and host, a host label that looks random as ***) when mendwright notify takes it, else only why it does not, such as (set, not a URL mendwright notify takes: it does not start with https://), and shows MENDWRIGHT_KEY and other unlisted MENDWRIGHT_* variables as (set, value hidden).
./.mendwright/overrides.json(orMENDWRIGHT_STORE): selectors, their status, and each key's role, expected text, destructive flag,intent=text, fingerprint and list fingerprint. It holds no page HTML, but it can quote page text: a healed XPath often anchors on a label (for examplecontains(normalize-space(.),'accept the terms')), and atext=you pass is saved asexpectedText.- With
heal_get_by=True, the key of eachheal.page.get_by_*()call is the call itself, so the call's arguments (its role, name, text or test id) are saved as keys and selectors inoverrides.json, and askeyandoldinjournal.jsonl. They come from your code, not the page, but a name in a call can be personal, such asget_by_text("Ada Lovelace"). A fingerprint is saved for each call that matched, as for every selector key, with the limits below. - Since 0.11.0, a heal of a
get_by_*call key (and, withsuggest_get_by=True, of any key) can be saved withgetBy, aget_by_*call that matched the healed element when the heal was made, built from the element's ARIA role and accessible name, its label or its test id, such asget_by_role("button", name="Log in", exact=True), and the same text is theget_byfield of itsheal.appliedevent injournal.jsonl. It is page text, as a healed XPath's label can be, up to 100 characters. mendwright never runs it;mendwright fixprints it, and the GitHub Action's pull request shows it in a code span, only when it is a call in mendwright's own spelling. - Since 0.10.0, for a key whose element was last found inside an open shadow root,
shadowHosts: a CSS selector for each shadow host around it, its tag with a test id, an id or a name it has, such asmy-app[id="root"], or its tag alone, and the same on each heal made inside a shadow root, whose selector is the chain of those hosts and a CSS selector. They are attribute values of the page, as the fingerprint's are. - The pages each key was found on:
lastPageandpages(paths in lower case), and since 0.9.0lastUrl, the address of the last one with its origin and the path's own case, such ashttps://shop.example/Orders/*/. Record ids, tokens (a JWT, a reset link's code), email addresses, percent-encoded text and names longer than 64 characters are saved as*, and it never holds user info, a query or a fragment. An origin can name an internal host (http://10.0.3.4:8080). No address is saved forfile://pages.mendwright checkreads this field, and loads it only on a site you name. - The fingerprint is saved for every key whose selector matched, not only for healed ones, while the fingerprint tier is on (the default). It holds the element's tag, its id, name, type, role, test ids,
aria-label,placeholder,title,altandautocomplete, the path of its link (no host, query or fragment), its non-generated class names, up to 80 characters of its visible text or of a form field's label (runs of 12 to 19 digits become#), and the tag, id, classes and role of up to 4 ancestors. It never holds an input's value. Visible text can still be personal, such as a name on an account page, so review the file before you commit it, or setMENDWRIGHT_FINGERPRINT=0. The fingerprint is never sent to a hosted model or a gateway.LocalModelProvider, which runs on your machine, gets it, redacted like the expected text. - A list fingerprint is saved for every
heal.locator_all()lookup that matched two or more elements while the fingerprint tier is on.heal.page.locator(...).all()andheal.driver.find_elements()save nothing. It holds the fingerprints of three of the items, as above, the tag names inside each of them and how many of each, how many items there were, and the tag, id, test id,aria-label, non-generated class names and role of their container. Once a lookup withnonempty=Truerecorded it, it also holds the list's heading oraria-label(up to 80 characters) and where the list sat: 4-byte hashes of the tags, ids, test ids and classes of its container and the elements around it, of up to 1,000 other elements of the page, and of up to 200 of its headings and labels. The hashes are short and can be guessed from a dictionary, so they don't keep that page text secret. It is never sent to any provider, and no provider is asked about a list. - A list heal that
heal.locator_all(..., nonempty=True)proposes saves what you check it by: two headings, two container summaries (tag, id, non-generated classes), the text of two recorded items and of two items found (each up to 80 characters, runs of 12 to 19 digits as#), the page's path and the selectors that matched nothing, at most 3 per key. The list fingerprint already holds item texts, so the kind of data is not new, but there is more of it, and it goes into the CI cache with the store.mendwright listsandmendwright reportprint these texts, so CI logs hold them; the error a lookup raises prints the headings, containers and selector, but no item text. Every control character in the page text a proposal saves, and in whatmendwright listsand the list messages print, keys included, is turned into?, apart from the first line of the error a lookup raises,No matching element for: KEY, which names the key as your code passed it, as in 0.8.0; a command for a key with one saysKEYand asks you to quote the key.mendwright lists acceptandrejectchange only the rows they are about, and accepting never makes a selector run: only your code does. journal.jsonlnext to it: heal events with the key, selectors, provider class, model, latency, and the model'sreasoningtext, which can quote the page. Aheal.heldevent of a proposed list heal holds its heading too. Withdismiss=True, aflow.dismissevent also holds up to 60 characters of the popup's text (runs of 12 to 19 digits redacted) and the labels of the buttons mendwright clicked on it, as the WARNING line does. On POSIX it's created with mode 0600.- Since 0.10.0, each journal event has
run, the run's id (MENDWRIGHT_RUN_ID, or on GitHub Actionsgh-<run id>.<attempt>). Aheal.declinedevent holds thereasona heal did not apply, the text the log line and the error give, which can quote the page, asreasoningcan. Aflow.stopevent holds a code, a key and a step or page path, never the message, so no popup text. overrides.json.lockwhile a write is in progress.- Since 0.11.0,
keys/<first 32 hex digits of sha256(key)>.jsonnext tooverrides.json(<file name>.keys/for another file name): one coordination record per key, with the key itself, the times of the last hour's model attempts, the model's declines per page signature with theirreason, which can quote the page, and, while a heal runs, a lease whose owner is the host name, the process id and a random part. They hold no heal, and the guide's cache and workflow leave them out. - With a remote store (a store URL in
MENDWRIGHT_STOREorstore_path=),overrides.jsonand the key recordskeys/<hash>.jsonare in the S3 bucket or behind the HTTPS server, with the fields above, andjournal.jsonlstays on each machine, inMENDWRIGHT_JOURNAL_DIR(default./.mendwright). Anyone who can write the store can change the selectors every worker uses, so give the other workers credentials that can only read (an S3 role withs3:GetObjectand theListBucketstatement; the reference server has no read-only token yet), and setMENDWRIGHT_STORE_READ_ONLY=1on them so they don't try to write. The variable alone only stops that worker writing; it is no access control. Credentials come from the AWS credential chain orMENDWRIGHT_STORE_TOKEN/MENDWRIGHT_STORE_TOKEN_FILE, never from the URL, which is refused when it holds one, and no message, log line or journal event holds them. - The TypeScript package in
js/writes the same two files in the same format,shadowHosts,run,heal.declinedandflow.stopincluded. - A
heal.canaryin the journal says what confirmed the heal and, for a passing test, holds the test's node id, which can hold a parametrized test's values. - Uploaded as an artifact for the GitHub Action, the store and the journal can be downloaded by anyone who can read the repository, until the artifact expires. The guide's workflow uploads only the two files, for one day.
A store file that is corrupt or can't be read raises an error. mendwright never treats it as empty and overwrites it. A remote store it can't read refuses every heal instead, and is never written over.
The action (action.yml at the root of this repository) runs mendwright fix --apply on the commit your tests ran on and opens a pull request with the result. The store and the journal decide which strings go into your code, so whoever can write them can propose a change. What the action does about that:
- It uses only stores from runs on
push,scheduleandworkflow_dispatch, and fromworkflow_runruns those started in the same repository. It takes the artifact only from its own run or the run that triggered it; no input names another run. It skipspull_request(a dry run is allowed there) andworkflow_runruns that a pull request or a fork started, and it refusespull_request_target, where a job has a write token and your secrets while it can see a fork's changes. - It refuses a checkout that is not the commit the tests ran on, or that has changes to tracked files, and it opens pull requests only into the branch the tests ran on, the default branch unless
only-default-branch: false. - It runs no code from your repository. The job runs no test, no browser and no package build: lxml is one wheel, installed with
--only-binary=:all: --require-hashesfromaction/requirements.txt, which pins its version and the hash of every file of that release, and mendwright is imported from the action's own folder. Its scripts run withpython -I,mendwright fixonly tokenizes and parses your files, and no git hook runs. - It commits only the tracked files
mendwright fixwrote, aftermendwright fixchecked that each Python file changed only in the planned strings. Never anything under.github/(mendwright fixskips those folders, and the action checks again), and never the store, the journal or the artifact, which stay outside the workspace. - It never pushes over a commit it did not make, pushes with
--force-with-lease, and pushes to the repository's own URL, not toorigin. - The token reaches git only through
GIT_CONFIG_*environment variables, never.git/configor an argument list, and its base64 form is masked before first use. Only the steps that call the API or push get it; the install andmendwright fixsteps don't. - No
${{ }}expression is part of a script: every input reaches a step through its environment. Text from the store, the journal, the artifact or your files is printed between::stop-commands::markers, so it can't act as a workflow command, and in the pull request and the job summary it is in code spans and code blocks, with bidirectional controls written out. Titles and commit messages hold no store text. - A person merges the pull request. The action never merges, approves or turns on auto-merge.
What review still has to catch: a forged store can name any string literal in your Python files that isn't a bare word as a key's old selector, and propose any XPath that compiles in its place, with a forged canary to confirm it. mendwright fix refuses a heal mendwright could not have saved, but an XPath such as //cdn.example/x.js is also a protocol-relative URL. With --glob, the same holds for quoted strings in other files. The pull request lists every replacement with its old and new value and where it is; read it as you would read a change from a contributor. The action is as trustworthy as everything that runs in your test job.
mendwright notify and the action's webhook-url post a summary of one run to a webhook. A webhook URL is a secret: its path is the key that lets anyone post to your channel, and for some receivers, such as AWS Lambda function URLs and Pipedream, its host is.
- The URL comes only from the environment variable
MENDWRIGHT_WEBHOOK_URL. No option takes it, since a command line shows in process lists, shell history and CI logs. - It must be
https://with a host;http://only forlocalhost,127.0.0.1and[::1]. Anything else is refused before any request. TLS is verified with Python's default context, andHTTPS_PROXYis honoured as urllib does. - It is never printed. Messages name the webhook by its scheme and host, with a label of the host that looks random (12 characters or more with a letter and a digit, or 20 or more), such as the id in
https://<id>.lambda-url.us-east-1.on.aws/, written as***. Of the webhook's answer to a failed POST, a message shows only one word of lower-case letters and underscores, such as Slack'sno_service, that is no part of the URL; any other answer is left out, since a receiver can quote its key in a form mendwright can't know. As a backstop, the URL, its path, its query, its user info, each path segment and query value of 8 characters or more, and such a label of the host are written as***in everything mendwright prints, in any case, as they are and percent-decoded, percent-encoded and JSON-escaped.mendwright doctorshows it as messages name the webhook whenmendwright notifytakes it, and else only why it does not, never the value: a token pasted without the rest of the URL is not read as a host. - No redirect is followed: a 3xx is a failure. At most two attempts are made, the second after 2 seconds, on HTTP 429, a 5xx, a timeout or a connection error, and each is cut off after
--timeoutseconds (10 by default), however slowly the webhook answers. - In the action, only the step that sends the alert gets the URL. It masks the same texts with
::add-mask::before it prints anything, and it runs only for a run the guard let through, never onpull_request. - A webhook that fails never fails the command or the job (
--fail-on-errormakes the command exit 1).
What is sent: counts, codes, mendwright's own phrases, key names, the title (the repository, or --title), the links to the run and the pull request, how the tests ended and mendwright's version. What is never sent: page HTML, input values, selectors (old or healed), the model's reasoning, the reason text of a heal that did not apply, popup text and button labels, page paths, interruption selectors, test node ids, store and journal paths, and provider and model names. mendwright notify reads only the journal, which holds no page HTML and no input values. The keys of heal.page and heal.driver calls are the selectors in your code, such as #email, and a key can name something personal; --detail counts (notify-detail: counts) leaves every key out. In Slack text, &, < and > are written as entities, so a key can't mention @channel or a person.
- The live check and the retried action are separate calls. The page can change between them.
- With
heal_get_by=True, a call that has a fingerprint can be healed onto an element that carries the fingerprint's id or test id but is now another control. The fingerprint tier's rules and the ARIA role check of aget_by_role()call limit that, but can't judge intent. - A
get_by_role()name matches part of the accessible name unless the call passesexact=True, so after a redesign the call itself can find another element ("Save" matches "Save draft"). mendwright doesn't check the code's own match. - Only keys you mark are refused as destructive. An unmarked key can be healed onto a control that pays, submits or deletes, as long as it has the key's role. That holds for a fingerprint heal too, which needs no model and no key, though its rules ask for the same numbers and words in a text and refuse a match outside the recorded containers.
- With
heal_get_by="model", aget_by_*call is healed by a model when its fingerprint isn't confident, so the same limits as for any model heal apply to it, and a model can pick a plausible wrong element that has the call's role. A call that never matched has no page the page guard can compare. Mark pay, submit and delete calls withdestructive_keys=(mendwright.get_by_key()builds their keys). - A hostile page can steer the model toward a different element with the same role. The checks limit the role, the match count and visibility, but can't judge intent. Use
expect=on actions that matter. - A direct provider holds its API key in your process. Without the google-genai SDK,
GeminiProviderfalls back to a REST call that puts the key in the URL query (?key=), so a proxy that logs URLs can record it. Installmendwright[gemini]to have the SDK make the call instead.GatewayProviderkeeps the model key on your server. mendwright doctorprints absolute paths for Python, the package and the store, which can contain your user name.- With the pytest plugin, a passing test confirms its heals. For
GatewayProviderthat is an HTTP call to/heal/outcome. A test that passes for the wrong reason confirms a wrong heal. - With the GitHub Action, such a heal reaches a pull request. Its description says to check each selector.
- Anything that can write the test job's files can forge the store and the journal; the pull request is then the only check.
- A collaborator who can push to the repository can push code that writes a store on
push. The action trusts collaborators as much as the repository does. - With a personal access token or a GitHub App token, the action's push and pull request start workflows. It skips a run on its own branches, so it doesn't loop, but your other workflows run on the branch.
- An alert names keys, unless
--detail counts. Anyone who reads the channel reads them, and the webhook's service keeps them. - A key name or a selector in the pull request can hold words such as
closes #5. GitHub doesn't act on them in a description, but a squash merge that copies the description into the commit message can close that issue.
If you find a way to get a selector past the checks, to heal a key marked destructive, or to send more page data than this page describes, please report it privately as described at the top.