Skip to main content
Glama
README.md
# browserskill

Расширения для Firefox и Chrome + CLI + скилл для Claude Code и Codex, дающие агентам полный доступ
к твоим настоящим браузерам. Никакого MCP-сервера: агент вызывает CLI как обычную команду,
а как ей пользоваться — описано в скилле. Транспорт целиком локальный: **stdio и Unix-сокет, ни одного HTTP-запроса
и ни одного открытого порта**.

## Как устроено

```
                                ┌─ Unix-сокет ~/.browserskill/firefox.sock ─▶ browserskill-host.js ─▶ расширение Firefox
агент ──скилл──▶ browserskill.js
                                └─ Unix-сокет ~/.browserskill/chrome.sock ──▶ browserskill-host.js ─▶ расширение Chrome
```

У каждого браузера свой native-хост и свой сокет (права `0600`). Хост поднимается самим
браузером при старте расширения и работает как брокер: к нему подключаются вызовы CLI.
Поэтому **несколько агентов одновременно** работают с одним браузером, а внешне не открыт
ни один сетевой порт.

Один CLI обслуживает оба браузера — какой использовать, задаёт флаг `-b chrome|firefox`
или переменная `BROWSERSKILL_BROWSER`; без них берётся единственный запущенный.

Ответы больше ~700 КБ (скриншоты, дампы DOM) автоматически режутся на чанки и собираются
обратно в хосте — ограничение native messaging на размер сообщения обходится прозрачно.

## Установка

Одной командой (macOS):

```bash
curl -fsSL https://raw.githubusercontent.com/ruslankolmakov/browserskill/main/get.sh | bash
```

`get.sh` клонирует репозиторий в `~/.browserskill/app` (другой путь — переменная
`BROWSERSKILL_SRC`), ставит `node` через Homebrew, если его нет, запускает `install.sh`,
открывает страницы расширений в найденных браузерах и кладёт путь к расширению в буфер
обмена — остаётся нажать «Загрузить распакованное». Повторный запуск обновляет установку.

Из клона вручную:

```bash
git clone git@github.com:ruslankolmakov/browserskill.git ~/Projects/browserskill
cd ~/Projects/browserskill
./install.sh
```

`install.sh` кладёт манифесты native-хоста в каталоги Firefox и всех найденных браузеров на
Chromium, создаёт лаунчеры с абсолютным путём к `node` (браузер из Finder не видит
`/opt/homebrew/bin`) и ставит скилл для обоих агентов:

- Claude Code: `~/.claude/skills/browserskill` (другой каталог — `CLAUDE_SKILLS_DIR`).
- Codex: `~/.codex/skills/browserskill` (учитывается `CODEX_HOME`; другой каталог — `CODEX_SKILLS_DIR`).

Затем ставим расширения — нужно хотя бы одно.

**Firefox:**

1. `about:debugging#/runtime/this-firefox`
2. **Загрузить временное дополнение…** → выбрать `extension/manifest.json`

> Временное дополнение живёт до перезапуска Firefox. Чтобы поставить навсегда, нужен
> Firefox Developer Edition / Nightly / ESR с `xpinstall.signatures.required = false`
> в `about:config` — тогда собери `.xpi` (`npm run build-xpi`) и установи через
> `about:addons` → **Установить дополнение из файла**. В обычном релизном Firefox
> неподписанные расширения навсегда поставить нельзя — это ограничение Mozilla.

**Chrome:**

1. `chrome://extensions` → включить **Режим разработчика**
2. **Загрузить распакованное расширение** → выбрать каталог `extension-chrome`
3. ID должен получиться `mifolcjccjkcdfdoebmkgklimogoemng` — он зашит полем `key`
   в манифесте, и именно его разрешает манифест native-хоста
4. Желательно: в карточке расширения включить **Разрешить пользовательские скрипты**

Последний пункт нужен для `eval` на сайтах со строгим CSP — подробности ниже.
Распакованное расширение переживает перезапуск Chrome, но требует включённого режима
разработчика.

Проверка обоих браузеров сразу:

```bash
./bin/browserskill.js doctor
./bin/browserskill.js browsers
```

## Подключение к агентам

Отдельной настройки нет: `install.sh` ставит общий `skill/SKILL.md` для Claude Code
и Codex, подставляя абсолютный путь к репозиторию. Скилл описывает, как звать CLI,
какие операции есть и на какие грабли не наступать.

В Codex скилл можно вызвать явно: `$browserskill`, либо выбрать через `/skills`.
Если новый скилл не появился, перезапусти Codex. Для уже установленного browserskill
достаточно повторно запустить `./install.sh`.

Любому другому агенту достаточно уметь запускать команду:

```bash
node /путь/к/browserskill/bin/browserskill.js <команда>
```

## Выбор браузера

```bash
browserskill browsers                  # кто доступен
browserskill -b chrome tabs            # разовый вызов в Chrome
BROWSERSKILL_BROWSER=chrome browserskill status
```

Без флага браузер выбирается сам: единственный запущенный, при двух живых — Firefox.
Флаг всегда сильнее переменной окружения. Состояния «закреплённый браузер» между вызовами
нет — в отличие от рабочего окна, которое живёт в расширении.

## Команды и операции

Короткие подкоманды (`browserskill` без аргументов): `doctor`, `browsers`, `status`, `windows`,
`use-window`, `new-window`, `tabs`, `open`, `snapshot`, `read`, `eval`, `api`, `call`.

Всё остальное — через `call <op> '<json>'`, это ~40 операций моста:

| Группа | Операции |
|---|---|
| Рабочее окно | `session.newWindow`, `session.useWindow`, `session.target`, `windows.list/open/close` |
| Вкладки | `tabs.list`, `tabs.get`, `tabs.open`, `tabs.navigate`, `tabs.close`, `tabs.activate`, `tabs.reload`, `tabs.back`, `tabs.forward`, `tabs.waitLoad` |
| Страница | `page.snapshot`, `page.text`, `page.html`, `page.eval`, `page.click`, `page.fill`, `page.press`, `page.waitFor`, `page.find`, `page.screenshot` |
| Отладка | `logs.console`, `logs.network` |
| Профиль | `cookies.list/set/remove`, `history.search/delete`, `bookmarks.search/tree/create/remove`, `downloads.list/start`, `storage.get`, `containers.list`, `sessions.recent/restore` |
| Всё остальное | `browser.call`, `browser.describe` |

Полный справочник с примерами вызовов — в `skill/SKILL.md`.

**`api` — это и есть «полный доступ».** Он вызывает любой метод WebExtension API
напрямую, так что агент не ограничен готовым списком:

```bash
browserskill api tabs.query '[{"audible":true}]'
browserskill api browsingData.removeCache '[{}]'
browserskill api proxy.settings.set '[{"value":{"proxyType":"none"}}]'
```

`call browser.describe` показывает, что вообще доступно (`{"path": "cookies"}` → список
методов). Наборы API у Firefox и Chrome разные, так что спрашивать стоит у того браузера,
в котором собираешься работать.

### Рабочее окно

По умолчанию операции без явного `tabId` идут в активную вкладку текущего окна — то есть
туда, где сейчас работаешь ты. Чтобы агент не мешал, закрепи за ним окно:

```bash
browserskill new-window https://example.com   # своё окно, сразу закреплено
browserskill use-window 3                     # закрепить существующее
browserskill call session.useWindow '{}'      # показать текущее закрепление
browserskill use-window release               # снять
```

После закрепления в это окно уходят все операции без `tabId`, туда же открываются новые
вкладки, а `tabs` показывает только его вкладки (`call tabs.list '{"allWindows":true}'` — все).
В `windows` закреплённое окно помечено `target: true`, а в попапе расширения видно
строкой «Рабочее окно». Закрепление живёт отдельно в каждом браузере и снимается само,
если окно закрыть.

<img src="docs/popup.png" alt="popup browserskill: состояние моста" width="300" />

### Работа со страницей

`snapshot` возвращает интерактивные элементы с короткими `uid` — это дешевле
скриншота и точнее селекторов:

```json
{ "uid": "e12", "tag": "input", "type": "email", "text": "Email address", "rect": {...} }
```

Далее `call page.fill '{"uid":"e12","value":"..."}'` и `call page.click '{"uid":"e15"}'`.
`page.fill` выставляет значение через нативный сеттер прототипа и шлёт `input`/`change`,
поэтому корректно работает с React, Vue и Svelte.

`eval` выполняет код в контексте страницы — это тело async-функции, доступны
`await` и `return`:

```js
return [...document.querySelectorAll("h2")].map(h => h.innerText)
```

Логи консоли и необработанные исключения собираются автоматически на каждой загруженной
странице (буфер на 2000 записей), сетевые запросы — через `webRequest`. Читать через
`call logs.console` и `call logs.network`. Вывод `console.*` из самого `eval` тоже
попадает в буфер.

## Чем Chrome отличается от Firefox

Расширение для Chrome — Manifest V3, и часть вещей там устроена иначе. Набор операций
одинаковый, отличается поведение:

| | Firefox | Chrome |
|---|---|---|
| Фон | постоянная фоновая страница | service worker, который браузер усыпляет |
| Произвольный JS | `tabs.executeScript` со строкой | мир страницы + `new Function`, при отказе CSP — мир пользовательских скриптов |
| Скриншот всей страницы | одним вызовом | прокрутка и сшивка кусков (медленнее, до 25 экранов) |
| Скриншот неактивной вкладки | как есть | вкладку приходится на миг активировать |
| Контейнеры | `containers.list` работает | аналога нет, вызов честно падает |

Что из этого важно на практике:

- **`eval` и CSP.** Код выполняется в мире страницы, а там действует её
  Content-Security-Policy. На сайтах со строгим `script-src` (например GitHub) `eval`
  запрещён. Обход — включить в карточке расширения **«Разрешить пользовательские
  скрипты»**: тогда browserskill выполнит код в отдельном мире, где CSP страницы не действует.
  Без этого на таких сайтах придёт понятная ошибка с этой же подсказкой.
  Операции `page.click`, `page.fill`, `page.snapshot` и остальные структурные
  операции CSP не касается вообще — они внедряются готовыми функциями, а не строками.
- **Буферы логов и сон воркера.** Service worker MV3 живёт от события до события. Расширение
  держит его будильником и переподключается автоматически, но если Chrome всё же усыпит
  воркер, накопленные `logs.console` и `logs.network` обнулятся. Закрепление рабочего
  окна переживает это — оно лежит в `storage.session`.
- **Служебные страницы.** `chrome://*`, Chrome Web Store и PDF расширениям недоступны —
  там любая операция со страницей вернёт ошибку. В Firefox то же самое с `about:*`
  и `addons.mozilla.org`.

## Примеры CLI

```bash
./bin/browserskill.js doctor                      # диагностика обоих браузеров
./bin/browserskill.js browsers                    # кто сейчас на связи
./bin/browserskill.js status                      # версия браузера, число вкладок
./bin/browserskill.js tabs                        # список вкладок
./bin/browserskill.js open https://example.com
./bin/browserskill.js snapshot 12                 # элементы вкладки 12
./bin/browserskill.js eval 'return document.title'
./bin/browserskill.js api tabs.query '[{"pinned":true}]'
./bin/browserskill.js call page.text '{"tabId":12}'
```

Браузер выбирается флагом `--browser=chrome` (или `-b chrome`), переменной
`BROWSERSKILL_BROWSER`, иначе автоматически:

```bash
./bin/browserskill.js -b chrome tabs
BROWSERSKILL_BROWSER=chrome ./bin/browserskill.js status
```

## Тесты

```bash
./tests/run.sh
```

Живой браузер не нужен — браузеры и расширения заменяются заглушками. Проверяются: проход
вызова CLI → сокет → хост → расширение, выбор браузера флагом, переменной и автоматически
с маршрутизацией между двумя хостами, сборка чанкованных ответов, очистка сокета,
отсутствие синхронных циклов при загрузке фона (отдельно для фоновой страницы Firefox
и для service worker Chrome) и, отдельно, устойчивость обоих перехватчиков консоли
к зацикливанию.

Последний тест не декоративный. В первой версии обмен со страницей шёл через
`window.postMessage`, а его видит и сама страница: если её код логирует полученные
сообщения (частый паттерн у виджетов и аналитики), возникала петля
`console.log → postMessage → обработчик страницы → console.log`. Каждая итерация уходила
в фон отдельным `runtime.sendMessage`, забивая главный поток родительского процесса
браузера, и он переставал реагировать на ввод. Сейчас обмен идёт приватным
`CustomEvent`, стоят защита от реентрантности, лимит 100 сообщений в секунду со страницы,
батчинг раз в 250 мс и общий потолок 500 записей в секунду в фоне.

## Безопасность

Это по построению очень мощный доступ: агент видит твои cookies, историю и авторизованные
сессии и может действовать от твоего имени на любом сайте.

### Подтверждение подключения

Каждое новое подключение клиента к сокету пользователь подтверждает вручную. Пока
не подтверждено — все вызовы (кроме health-check `ping` и `__status`) висят, а хост
показывает нотификацию браузера. Клик по ней открывает popup расширения, где для запроса
предлагается выбор области:

<img src="docs/approval-popup.png" alt="popup browserskill с запросом доступа" width="300" />


- **это подключение** — доступ только для текущего живого соединения; переподключился клиент — спросят снова;
- **на эту сессию** — до перезапуска браузера (запоминается токен клиента в памяти хоста);
- **навсегда** — токен клиента сохраняется в `~/.browserskill/allowed-<браузер>.json` (0600) и переживает перезапуск.

Одобрение действует в пределах одного браузера: разрешив агенту Firefox, ты не разрешил
ему заодно и Chrome — там он спросит отдельно.

Клиент идентифицируется стабильным токеном из `~/.browserskill/client-token` (0600) — он
общий для всех вызовов CLI с этой машины, поэтому одобрение «навсегда» даётся один раз. Если
пользователь не реагирует за `BROWSERSKILL_APPROVAL_TIMEOUT_MS` (по умолчанию 120000 мс), доступ
отклоняется (fail-closed). Полностью выключить гейт — `BROWSERSKILL_APPROVAL=0` (для headless/CI).

Ограничение платформы: кнопок в нотификациях Firefox нет, поэтому подтверждать нужно
именно в popup (нотификация только сигналит и открывает его).

- Сокеты лежат в `~/.browserskill` с правами `0600` — доступны только твоему пользователю,
  никакой сети, ни локальной, ни внешней.
- Буферы `logs.console` и `logs.network` пишутся постоянно, со всех вкладок, и в них
  оседает то, что страницы логируют сами. На практике это бывают почта, идентификаторы
  аккаунта, тариф и прочие персональные данные — например, `claude.ai` печатает в консоль
  полный набор трейтов аналитики. Любой подключённый агент прочитает это одним вызовом.
  Чистить буфер: `browserskill call logs.console '{"clear":true}'`.
- Манифест хоста ограничивает подключение конкретным расширением: `allowed_extensions`
  с `browserskill@local` у Firefox, `allowed_origins` с `chrome-extension://mifolcjccjkcdfdoebmkgklimogoemng/`
  у Chrome.
- Чтобы временно всё отключить — выгрузи расширение (`about:debugging` /
  `chrome://extensions`) или закрой браузер: сокет исчезает вместе с хостом.
- Логи хостов: `~/.browserskill/firefox.log` и `~/.browserskill/chrome.log`.

## Диагностика

| Симптом | Причина |
|---|---|
| `сокет не найден` | браузер не запущен либо расширение не загружено; `browserskill browsers` покажет, кто на связи |
| `расширение browserskill не подключено к хосту` | хост поднялся, но расширение отвалилось — жми «Переподключить» в попапе |
| Значок расширения показывает `off` | native-хост не запускается; смотри `~/.browserskill/<браузер>.log` и проверь путь к `node` в лаунчере |
| В Chrome ID расширения не `mifolcjccjkcdfdoebmkgklimogoemng` | загружен каталог без поля `key` в манифесте — переустанови из `extension-chrome` |
| `страница запрещает выполнение кода своей CSP` | включи «Разрешить пользовательские скрипты» в карточке расширения Chrome |
| `скрипт не вернул результат` | это внутренняя страница (`about:`, `chrome://`, магазин расширений) — там расширения работать не могут |
| Вызовы висят, значок показывает `!` | ждёт твоего подтверждения — открой popup browserskill и выбери область доступа |
| `доступ к … не подтверждён (таймаут)` | никто не нажал подтверждение за отведённое время; повтори вызов и подтверди в popup |
| Агент переспрашивает доступ каждый раз | одобрено «на подключение»; выбери «на сессию» или «навсегда» |
| В Chrome пропали логи консоли | service worker уснул и буферы обнулились — логи копятся заново с этого момента |

Maintenance

ActivityMaintained
ResponsivenessNo issues