How to set up FiltersAgent locally, run the checks, and work on the agent, the AdGuard CLI module and the action.
- Code style, architecture and Git conventions live in AGENTS.md; this document does not repeat them.
- Commands, environment variables and run output are listed in the CLI reference.
- Prerequisites
- Getting Started
- Development Workflow
- Common Tasks
- IDE Configuration
- Troubleshooting
- Additional Resources
| Tool | Version | Why |
|---|---|---|
| Node.js | 24 or newer (engines.node in package.json) |
Runtime |
| pnpm | 10.x (lockfile lockfileVersion: '9.0') |
The only supported package manager |
| An LLM endpoint | OpenAI chat-completions or Anthropic Messages API | Powers the agent loop and the vision review |
| A GitHub token | Public read access | Reading issues; not needed for offline runs |
gh, jq, perl |
Any recent | scripts/copy-upstream-issue.sh |
| Docker | With BuildKit | Building the action image (action.Dockerfile) |
1Password CLI (op) |
Optional | Passing the AdGuard licence without writing it to .env |
No global CLI install is required: everything runs through tsx and the local
node_modules. A local clone of
AdguardTeam/AdguardFilters,
named by REPOSITORY_PATH, is needed only for the repo commands.
node -v # v24 or newer
pnpm -vFrom the repository root:
pnpm installThe cloakbrowser Chromium binary downloads on the first browser launch; no
manual step is needed for it. Firefox-family runs and their tests use
Playwright's Firefox build, which you install once:
pnpm exec playwright-core install firefoxcp .env.example .env && chmod 600 .env.env.example is the variable inventory. The agent CLI (pnpm start),
pnpm test:e2e, pnpm benchmark:case:add and
pnpm benchmark:case:check load .env into the process
environment with plain dotenv; variables already exported in the shell win.
Every agent run needs a provider speaking the OpenAI chat-completions or
Anthropic Messages API: LLM_BASE_URL, LLM_API_KEY, LLM_MODEL and the
token limits. LLM_API defaults to openai-completions; set it to
anthropic-messages for Anthropic, with LLM_BASE_URL=https://api.anthropic.com
without /v1. LLM_VISION_MODEL is optional and follows the effective
reasoning model, including --model overrides.
There is no default provider, and a missing required variable fails the run
with a ConfigError that names it. The
benchmark guide shows a
complete .env and what each value must be.
pnpm benchmark:summarize warns with mixed pins: when agent revisions,
KnowledgeBase commits or browser builds differ across the compared runs.
Set GITHUB_TOKEN only for commands that read a live GitHub issue; a run from
an exported issue file does not need it.
Executable entry points read the environment once and pass it to pure loaders
such as loadConfig() in src/config/config.ts. The loaders take the
environment as an argument, so tests pass a fake one without touching
process.env.
pnpm start --help # list commands
pnpm start -- show issue 12345 # print the model-filled report
pnpm start -- fix issue 12345 --verbose # investigate one issue
pnpm start -- repo search --domain example.com-- separates pnpm's flags from the arguments forwarded to lab/main.ts. The
CLI reference lists every command, option and output
file.
The adguard_cli executor investigates reports from desktop AdGuard products
through AdGuard CLI running as a
filtering proxy. Local runs plug it in the same way the action does: as the
blocker module committed in adguard-cli/, named with --blocker-module. The
module needs two variables:
| Variable | Purpose |
|---|---|
ADGUARD_LICENSE_KEY |
AdGuard licence the CLI is activated with for each run |
ADGUARD_CLI_PATH |
Absolute path of the adguard-cli executable; without it the module runs adguard-cli/bin/adguard-cli |
To run it locally:
-
Download a stable release for your platform from https://github.com/AdguardTeam/AdGuardCLI/releases, one tagged
v<major>.<minor>.<patch>-release, and extract it. Do not use a pre-release (rc, beta, nightly), and do not run its installer. -
Set both variables in
.envor in the environment, and name the module:ADGUARD_CLI_PATH=/path/to/adguard-cli \ ADGUARD_LICENSE_KEY=<your-licence-key> \ pnpm start -- fix issue 12345 --browser on \ --blocker-module adguard-cli/blocker-module.json
To keep the licence out of
.envand the shell history, pass a 1Password reference through the 1Password CLI instead.op runresolves anop://reference only from the command line or an--env-file:ADGUARD_CLI_PATH=/path/to/adguard-cli \ ADGUARD_LICENSE_KEY='op://<vault>/<item>/<field>' \ op run -- pnpm start -- fix issue 12345 --browser on \ --blocker-module adguard-cli/blocker-module.json
Without --blocker-module a run has only the browser extension. Without
ADGUARD_LICENSE_KEY the run stops before it starts and names the variable.
The run executes the bundle, not the sources: after changing
src/adguard-cli/, rebuild it as described in
Build the AdGuard CLI module.
What a run does with the CLI (src/adguard-cli/adguard-cli-proxy.ts):
- It creates a fresh mode-0700 HOME for the CLI under
/tmp/agcli-. The control socket lives under HOME, and the CLI fails to start once that path exceeds the 104-byte socket path limit, so the HOME path is kept short. - It writes
proxy.yamlin manual proxy mode on a loopback port. The run's filter lists and the agent's rules go intofilters:as files instead offlm://, so the CLI executes exactly the lists the run downloaded. Certificate transparency stays on, the CLI's own default: with it off, the 1.5 pre-releases reject valid chains to roots that carry extra SCT constraints and tunnel those sites unfiltered. - It runs
adguard-cli activate <key>and checksadguard-cli license:activateexits 0 even when it only printed a login link. Then it runsadguard-cli certand declines the system-store install; no keychain or system trust store is touched. - It starts
adguard-cli start --no-forkand restarts it for every rule or list change. - Before the run gets the environment, it starts the CLI with no list and no
rule and sends up to two HTTPS requests to the issue's site through it
(
src/proxy-blocker/https-filtering-proof.ts). Only when the CLI answers one under its own authority is the environment ready, and only then does the browser accept that authority. Anhttp:site that serves no HTTPS at all passes with a plain HTTP answer that does not redirect to HTTPS; one whose HTTPS the CLI passes through unread does not. Otherwise the preparation ends withhttps_filtering_unproven, the run stays analysis-only, and the run log names what the requests saw and the CLI's own reason. - It runs
adguard-cli reset-licensewhen the run stops, and also when its own preparation fails after the activation, then deletes the HOME.
The licence key is passed to activate in argv, because the CLI reads it from
nowhere else, so it is briefly visible in the process list. The CLI host
replaces the key in every transcript it writes, and the run redacts
ADGUARD_LICENSE_KEY in its logs.
Every activation binds one licence device until its reset. To keep parallel runs on one device, activate the CLI once and pack its HOME as a seed:
ADGUARD_LICENSE_KEY=... scripts/seed-adguard-cli-home.sh /path/to/adguard-cli /tmp/agcli-home.tar.gzSet ADGUARD_CLI_HOME_ARCHIVE to the archive. The run extracts a private copy,
checks license, and filters without activating. It never resets a restored
HOME: every copy shares the seed's device, and a reset on one would unbind them
all. When the seed's licence is no longer active, the run log shows
seeded licence not active, activating instead, and the run activates and
resets on its own. scripts/pack-adguard-cli-home.sh repacks an existing HOME
without activating another device.
The seed holds licence state and the CLI's own CA key. Keep it out of places where untrusted code runs.
The seed takes one device; runs without a seed take one each while they run.
When the devices are exhausted, the run log shows
adguard cli activation failed with Maximum devices exceeded, the desktop
run ends without an evidence route, and extension runs are unaffected. A
licence reset FAILED line, or a killed run, leaves a device bound: unlink it
under Devices in the AdGuard account (https://my.adguard.com).
Development happens in AdGuard's internal repository; the public AdguardTeam/FiltersAgent repository is a read-only mirror of it, and pull requests opened there are not merged. Report problems in the public repository's issues.
In the internal repository, branch off master, keep each branch to one
change, and open the pull request against master. Branch, commit and pull
request names follow the Git workflow rules in AGENTS.md. Run
the checks below before requesting review; the checks workflow runs them on
every pull request.
| Script | What it runs |
|---|---|
pnpm start |
The local CLI (lab/main.ts) |
pnpm test |
The Vitest suite |
pnpm test:e2e |
The golden end-to-end cases (lab/e2e/run.ts), with a real LLM provider |
pnpm benchmark:case:add |
Adds a golden case (lab/e2e/case-add.ts) |
pnpm benchmark:case:check |
Checks or migrates historical cases without an LLM (lab/e2e/case-check.ts) |
pnpm benchmark:verify-run |
Verifies one benchmark run's output (lab/benchmark/verify-run.ts) |
pnpm benchmark:summarize |
Compares saved model series without a provider or browser (lab/benchmark/summarize.ts) |
pnpm lint |
oxlint plus the prompt-document scan |
pnpm fmt |
oxfmt plus eslint --fix on src/ and lab/ |
pnpm fmt:check |
The formatter checks without writing |
pnpm typecheck |
tsc over src/, lab/ and tests/ without emitting |
pnpm build |
tsc with emit to dist/ |
pnpm build:adguard-cli |
Rebuilds the committed AdGuard CLI module in adguard-cli/ |
The benchmark guide covers the test:e2e and
benchmark:* scripts.
Code style and architecture conventions are documented in AGENTS.md. The toolchain that enforces them:
pnpm lint # oxlint (code quality and JSDoc) plus the prompt-document scan
pnpm fmt # oxfmt plus eslint --fix on src/ and lab/
pnpm fmt:check # the same checks without writing, as CI runs them0 errors from pnpm lint is the success criterion; warnings do not fail it.
Tests use Vitest and live under the root tests/
directory, mirroring src/ and lab/ (src/repo/search.ts →
tests/repo/search.test.ts); read-only fixtures sit beside the tests that use
them. Write the failing test first, then the minimal implementation.
pnpm test # the whole suite, once
pnpm test tests/repo # one directory or filePaths that use Octokit are mocked, so the suite needs no GITHUB_TOKEN. The
pilot corpus integrity test reads the public human commit of every benchmark
case to prove that its rules are absent from agent-visible input, so the
complete suite needs network access. The test makes one shallow fetch per case
from the public AdguardFilters remote, one after another; an anonymous fetch
has taken 18–27 seconds, so this one test can take several minutes, and its
timeout is ten minutes.
CI runs pnpm vitest run without
tests/browser/strict-route-proxy-contract.test.ts: that suite launches a real
browser through the strict route, and the launch fails on the CI runner. Run it
locally when you change the strict route.
For changes to the extension route, this subset is a quicker regression gate than the complete suite:
pnpm exec vitest run \
tests/environment/filtering-environment-execution-recorder.test.ts \
tests/environment/browser-extension-environment.test.ts \
tests/environment/environment-result-projection.test.ts \
tests/browser/browser-session.test.ts \
tests/validator/phase-orchestrator.test.ts \
tests/validator/phase-capture.test.ts \
tests/validator/structural-snapshot.test.ts \
tests/orchestrator/agent-runtime.test.ts \
tests/orchestrator/fix-core.test.ts \
tests/types/fix-run-result.test.tsThe golden end-to-end cases are the benchmark: adding a case, running one or
all of them, reading the results and counting the cost are covered in the
benchmark guide. They call a real LLM provider and
are not part of pnpm test.
pnpm typecheck # type-check src/, lab/ and tests/
pnpm build # tsc with emit to dist/Nothing consumes dist/: the CLI runs through tsx, and the action boots its
sources through tsx inside the image action.Dockerfile builds.
Every fix run records a trace at artifacts/trace-<run-id>.json in its
investigation directory: every LLM turn and tool call in order. Add
--verbose for debug-level logs on stderr during the run, and set
HEADLESS=false to watch the browser.
backlog posts its reports as issue comments whenever GITHUB_TOKEN is set,
on whichever repository the identity resolves to. Pass --no-comment to keep
the reports local:
pnpm start -- backlog --repository AdguardTeam/AdguardFilters \
--limit 2 --no-comment --output-dir "$PWD/tmp/backlog-runs"Each issue's report is written to its own artifacts directory under
--output-dir.
The adguard-cli setup action, which a workflow plugs in as
uses: AdguardTeam/FiltersAgent/adguard-cli@<ref> once a release carries it,
is generated: scripts/build-adguard-cli-module.ts bundles the module process
from src/adguard-cli/ and copies the setup action from
modules/adguard-cli/action.yml together with the scripts it runs:
scripts/fetch-adguard-cli.mjs, which installs a pinned release and verifies
its checksum and signature, and the seed scripts.
GitHub reads adguard-cli/ from the repository at the referenced ref, so the
built module is committed. Rebuild it and commit the result whenever you change
src/adguard-cli/, a source module it imports, modules/adguard-cli/, one of
the copied scripts, or a dependency the bundle includes:
pnpm run build:adguard-cli
git add adguard-cli/The build empties adguard-cli/ and writes it again. Its output depends only on
the sources and the lockfile, so two builds give byte-identical files. The
Checks workflow rebuilds the module and fails when the result differs from the
committed copy.
The action image leaves src/adguard-cli/ out, so the action itself never
carries the CLI code. Current local CLI runs load the bundle through
--blocker-module adguard-cli/blocker-module.json; see
Run the AdGuard CLI executor. Historical
local runs refuse --blocker-module, and the benchmark (pnpm test:e2e) runs
without modules, because module lists cannot be pinned to the reporter's date.
A fork of AdguardTeam/AdguardFilters gives you real reports without touching
the upstream issues. A workflow in a public fork can only use a published
release of the action, so an unreleased change runs through the local CLI
against the fork's issues.
-
Fork
AdguardTeam/AdguardFiltersand enable Issues in the fork's settings; forks start with Issues off. -
Copy an upstream issue into the fork:
scripts/copy-upstream-issue.sh 239744 <you>/AdguardFilters --no-label
The script copies the body and the human comments and wraps every mention and issue reference in a code span, so nothing notifies upstream.
--no-labelleaves out thefilters-agentlabel that starts a connected action. -
Run your change on the copied issue:
pnpm start -- fix issue <number> --repository <you>/AdguardFilters
The report and the full evidence land in the output directory the CLI reference describes.
To try the released action on the fork, enable Actions as well, disable the
workflows the fork inherited (gh workflow disable <workflow> --repo <you>/AdguardFilters), connect it as the
GitHub Action guide
describes, and copy an issue without --no-label. Keep the base URL in a
secret too: workflow artifacts of a public fork are readable by anyone.
JetBrains IDEs import the TypeScript configuration and the ESLint setup from the repository automatically.
VS Code has no committed workspace settings. Install the TypeScript and
ESLint extensions and enable ESLint format-on-save with the flat config
eslint.config.js. oxfmt has no VS Code extension yet, so run pnpm fmt
before committing.
You ran pnpm start -- --help. Use pnpm start --help, or
pnpm start <command> <subcommand> --help for one command.
A required environment variable is missing or malformed. Agent runs require
LLM_BASE_URL, LLM_API_KEY, LLM_MODEL and the token limits;
LLM_VISION_MODEL follows the effective reasoning model, including overrides.
Commands that read GitHub also require GITHUB_TOKEN and a repository identity.
The error message names every missing or invalid variable.
repo search and repo map need a local checkout of the filters repository:
git clone https://github.com/AdguardTeam/AdguardFilters.git ../AdguardFilters
export REPOSITORY_PATH="$PWD/../AdguardFilters"The run has no repository to read the lists from: REPOSITORY_PATH is unset
or points at a checkout without a parseable origin remote, and no repository
is named. Set REPOSITORY_PATH to a git checkout of the filters repository or
pass --repository <owner>/<repo>.
adguard-cli license reported no active licence after activate. The log
entry carries both commands' output. With Maximum devices exceeded the
licence has no free device: see Licence devices. Otherwise
check ADGUARD_LICENSE_KEY.
GITHUB_TOKEN is missing, expired, or lacks read access to the repository.
Regenerate the token with at least public repository read access.
- AGENTS.md — code style, toolchain, and architecture conventions
- README.md — what the project is and how to start
- CHANGELOG.md — version history
- DEPLOYMENT.md — what a release contains and how to cut one
- CLI reference — commands, environment variables and run output
- Action inputs — every input of the action
- GitHub Action guide — connect and configure the action
- Benchmark guide — golden cases and the benchmark
- Runtime architecture — the agent loop, environment selection, input limits and local publication
- Browser with an extension — how the extension routes drive a browser
- Publication comment format — the register and layout of the issue comment
- REQUIREMENTS.md — product requirements and design notes
- TODO.md — open work: what the agent does not do yet
.env.example— every supported environment variable