# How to Set Up Playwright Browser Automation with Domain Allow‑Listing in gh‑aw

> Set up Playwright browser automation with domain allow-listing in gh-aw by declaring an allowed_domains array in your workflow. Ensure secure outbound traffic with MCP server enforcement.

- Repository: [GitHub/gh-aw](https://github.com/github/gh-aw)
- Tags: how-to-guide
- Published: 2026-02-16

---

**To set up Playwright browser automation with domain allow‑listing in gh‑aw, declare an `allowed_domains` array in the workflow front‑matter under the `playwright` tool configuration; the MCP server will enforce this list at runtime, blocking all outbound traffic to non‑listed domains.**

The **gh‑aw** extension embeds Playwright‑based browser automation directly into agentic workflows on GitHub Actions. To prevent unauthorized outbound network calls, gh‑aw implements a strict security model that requires an explicit allow‑list of domains the Playwright container may contact. This guide explains how to configure, compile, and verify domain allow‑listing using the actual source implementation.

## Architecture of Domain Allow‑Listing in gh‑aw

### Core Components

The allow‑list mechanism spans the parser, compiler, and runtime layers:

| Component | Responsibility | Key Source |
|-----------|----------------|------------|
| **Tool definition** – `PlaywrightToolConfig` | Holds Playwright‑specific configuration, including `allowed_domains`. | [[`pkg/workflow/tools_types.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/tools_types.go)](https://github.com/github/gh-aw/blob/main/pkg/workflow/tools_types.go#L275-L290) |
| **MCP Playwright config extractor** – `PlaywrightDockerArgs` & `AllowedDomains` | Reads the `allowed_domains` entry from the front‑matter and converts it into a slice of strings that the MCP server will enforce. | [[`pkg/workflow/mcp_playwright_config.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/mcp_playwright_config.go)](https://github.com/github/gh-aw/blob/main/pkg/workflow/mcp_playwright_config.go#L11) |
| **MCP parser** – `ParseMCP` | During front‑matter parsing, detects the `allowed_domains` key, resolves any GitHub Actions expressions, and stores the result in the tool config. | [[`pkg/parser/mcp.go`](https://github.com/github/gh-aw/blob/main/pkg/parser/mcp.go)](https://github.com/github/gh-aw/blob/main/pkg/parser/mcp.go#L455-L456) |
| **YAML compiler** – [`compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/compiler_yaml.go) | When the final workflow YAML is generated, the allow‑list is rendered as a JSON array under the `playwright` MCP tool configuration. | [[`pkg/workflow/compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go)](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go#L507-L509) |
| **Runtime enforcement** – MCP server / firewall | The MCP sandbox validates outbound HTTP(S) requests against the configured allow‑list, blocking any domain that is not listed. This is reflected in log/guardrail reports (`allowed_domains` fields). | [[`pkg/cli/logs_report.go`](https://github.com/github/gh-aw/blob/main/pkg/cli/logs_report.go)](https://github.com/github/gh-aw/blob/main/pkg/cli/logs_report.go) |

### Data Flow from Front‑Matter to Runtime

1. **User writes a workflow markdown file** with a `tools:` block that includes Playwright and an `allowed_domains` array.
2. The **parser** (`ParseMCP` in [`pkg/parser/mcp.go`](https://github.com/github/gh-aw/blob/main/pkg/parser/mcp.go)) reads the array, handling plain strings *or* `${{ secrets... }}` expressions.
3. The **MCP config extractor** ([`pkg/workflow/mcp_playwright_config.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/mcp_playwright_config.go)) builds a `PlaywrightDockerArgs` struct that stores the sanitized domain list.
4. The **compiler** ([`pkg/workflow/compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go)) inserts the list into the generated workflow YAML (`allowed_domains: [...]`).
5. At runtime, the **MCP server** only allows outbound traffic to those domains, providing a strong security boundary.

## Declaring the Allow‑List in Workflow Front‑Matter

Add a `tools` block to the top of your workflow markdown file:

```yaml
---
engine: copilot               # or claude, codex, custom

tools:
  playwright:
    version: "v1.41.0"       # optional – overrides default version

    allowed_domains:
      - "example.com"
      - "api.github.com"
      - "${{ secrets.ALLOWED_DOMAIN }}"   # can be a secret expression

---

```

Each entry may be a plain domain string, a wildcard pattern (`*.github.com`), or a GitHub Actions expression that resolves to a domain at runtime. If the array is omitted, gh‑aw defaults to `localhost`, effectively disabling external network access.

The parser validates the array in [`pkg/workflow/mcp_config_validation.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/mcp_config_validation.go), throwing an error if the format is invalid.

## Compiling and Running the Workflow

Compile the markdown into a GitHub Actions workflow:

```bash
gh aw compile path/to/your-workflow.md

```

The compiled `.github/workflows/*.yml` will contain a section similar to:

```yaml
steps:
  - name: Playwright MCP
    uses: github/gh-aw@vX.Y
    with:
      tools: |
        playwright:
          allowed_domains: ["example.com","api.github.com"]

```

Execute the workflow via the standard GitHub Actions run, or invoke `gh aw run` for a local test:

```bash
gh aw run path/to/your-workflow.md

```

Inspect the MCP logs to verify the allow‑list was applied:

```bash
gh aw mcp logs --workflow your-workflow

```

The JSON log output includes an `allowed_domains` field (see [`pkg/cli/logs_report.go`](https://github.com/github/gh-aw/blob/main/pkg/cli/logs_report.go)), confirming which domains were permitted during execution.

## Practical Examples

### Minimal Workflow with Static Domains

Create [`test-playwright-allowlist.md`](https://github.com/github/gh-aw/blob/main/test-playwright-allowlist.md):

```yaml
---
engine: copilot
tools:
  playwright:
    allowed_domains:
      - "example.com"
      - "api.github.com"
---

# A simple Playwright script that navigates to example.com

{{/* language=javascript */}}
const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  const page = await browser.newPage();
  await page.goto('https://example.com');
  console.log(await page.title());
  await browser.close();
})();

```

This compiles to a workflow that only permits outbound calls to `example.com` and `api.github.com`.

### Dynamic Domains via GitHub Secrets

```yaml
---
engine: copilot
tools:
  playwright:
    allowed_domains:
      - "${{ secrets.ALLOWED_API_DOMAIN }}"
---

# Playwright script that will hit whatever domain is stored in the secret

```

The parser resolves the expression during workflow execution, and the MCP server enforces the resolved value at runtime.

### Testing with the CLI

The repository ships a ready‑made test workflow: [`pkg/cli/workflows/test-playwright-args.md`](https://github.com/github/gh-aw/blob/main/pkg/cli/workflows/test-playwright-args.md). It demonstrates argument passing and the allow‑list in action. Run it locally:

```bash
gh aw run pkg/cli/workflows/test-playwright-args.md

```

## Summary

- **gh‑aw** embeds Playwright automation into agentic workflows but requires an explicit `allowed_domains` array for security.
- The allow‑list is declared in workflow front‑matter under `tools.playwright.allowed_domains` and supports plain strings, wildcards, and GitHub Actions expressions.
- Key source files handling the flow include [`pkg/workflow/tools_types.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/tools_types.go) (struct definition), [`pkg/workflow/mcp_playwright_config.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/mcp_playwright_config.go) (extraction), [`pkg/parser/mcp.go`](https://github.com/github/gh-aw/blob/main/pkg/parser/mcp.go) (parsing), and [`pkg/workflow/compiler_yaml.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/compiler_yaml.go) (YAML generation).
- Runtime enforcement occurs in the MCP server sandbox, which blocks any outbound request to domains not in the list.
- Verify configuration using `gh aw mcp logs --workflow <name>` to inspect the `allowed_domains` field in execution logs.

## Frequently Asked Questions

### What happens if I omit the allowed_domains array?

If you omit the `allowed_domains` array, gh‑aw defaults to `localhost`, effectively disabling external network access for the Playwright container. This prevents any outbound HTTP(S) requests to external APIs or websites, ensuring a secure-by-default posture.

### Can I use wildcards in domain allow‑listing?

Yes, you can use wildcard patterns such as `*.github.com` in the `allowed_domains` array. The MCP server interprets these patterns when validating outbound requests, allowing subdomains that match the specified wildcard while still blocking unrelated domains.

### How are secrets resolved in the allowed_domains list?

GitHub Actions expressions like `${{ secrets.ALLOWED_DOMAIN }}` are resolved by the parser in [`pkg/parser/mcp.go`](https://github.com/github/gh-aw/blob/main/pkg/parser/mcp.go) during the workflow compilation phase. The resolved string value is then passed through [`pkg/workflow/mcp_playwright_config.go`](https://github.com/github/gh-aw/blob/main/pkg/workflow/mcp_playwright_config.go) and rendered into the final YAML, where the MCP server receives the literal domain name at runtime.

### Where can I verify that the allow‑list was applied?

You can verify the active allow‑list by inspecting the MCP execution logs using the command `gh aw mcp logs --workflow <your-workflow-name>`. The JSON output includes an `allowed_domains` field (as defined in [`pkg/cli/logs_report.go`](https://github.com/github/gh-aw/blob/main/pkg/cli/logs_report.go)) that lists every domain permitted during that specific run.