-reins gives your coding agent (Claude Code, Cursor, Codex, Copilot — anything
-with a shell) control of the real, logged-in Chromium browser you already use,
-through a CLI and a Manifest V3 extension. No MCP server to register, no debug
+reins gives your coding agent control of the real, logged-in Chromium browser
+you already use, through a CLI and a Manifest V3 extension. Claude Code, Cursor,
+Codex, Copilot: anything with a shell. No MCP server to register, no debug
profile, no launch flags, no tokens.
## Quick start
@@ -38,35 +38,45 @@ npm i -g @karnstack/reins # the CLI (daemon included, starts on demand)
npx skills add karnstack/reins # the skill, into your agent(s) of choice
```
-Then install the extension in every Chromium browser you want agents to reach —
-Chrome, Brave, Edge, Arc, Dia:
+Then install the extension in every Chromium browser you want agents to reach:
+Chrome, Brave, Edge, Arc, Dia.
-**[Add reins from the Chrome Web Store](https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo)** — it finds the local daemon and connects on its own.
+**[Add reins from the Chrome Web Store](https://chromewebstore.google.com/detail/reins/hnjcfgochepemjndccfblpmfmlblkofo)**. It finds the local daemon and connects on its own.
-No store access? `reins extension` installs it via Load unpacked instead — see
-[docs/SIDELOAD.md](docs/SIDELOAD.md). That's the whole setup; `reins status`
-shows what's connected.
+No store access? `reins extension` installs it through Load unpacked instead.
+See [docs/SIDELOAD.md](docs/SIDELOAD.md). That is the whole setup, and
+`reins status` shows what is connected.
## The loop
```bash
-reins snapshot # list interactive elements with refs → e5: button "Submit"
-reins click --ref e5 # act by ref
-reins text # verify (or: reins screenshot → prints an image path)
+reins snapshot # look: interactive elements with refs → e5: button "Submit"
+reins click --ref e5 # act: by ref
+reins text # check: or reins screenshot, which prints an image path
```
-Every command takes `--tab ` (default: active tab), `--browser ` (only
-when several are connected), and `--json`. `reins help` is self-describing;
-`reins cdp` is the escape hatch to the full Chrome DevTools Protocol.
+Shared flags, on the commands that act on a page or a tab: `--tab `
+(default: active tab), `--browser ` (only when several are connected), and
+`--json` for raw output. `reins help` is self-describing, and
+`reins cdp` reaches the full Chrome DevTools Protocol when the curated commands
+are not enough.
+
+## Limits
+
+Chromium only, so no Firefox and no WebKit. No headless mode: reins drives a
+browser you already have open, which also makes it the wrong tool for CI. With
+two browsers connected, commands need `--browser `, because reins never
+guesses which one you meant. Releases are still `0.x`, so commands, flags, and
+output can change.
## Learn more
The full story lives on the site:
-- **[Docs](https://reins.tech/docs)** — getting started, architecture, and the complete command reference
-- **[How it compares](https://reins.tech/docs/comparison)** — vs agent-browser, dev3000, and playwright-mcp
-- **[Security](https://reins.tech/docs/security)** — per-site permissions, `127.0.0.1`-only binding, and the threat model
-- **[Site permissions](https://reins.tech/docs/permissions)** — the `deny` / `read` / `full` tiers and how to tighten them
+- **[Docs](https://reins.tech/docs)**: getting started, architecture, and the complete command reference
+- **[How it compares](https://reins.tech/docs/comparison)**: reins next to agent-browser, dev3000, and playwright-mcp
+- **[Security](https://reins.tech/docs/security)**: per-site permissions, `127.0.0.1`-only binding, and the threat model
+- **[Site permissions](https://reins.tech/docs/permissions)**: the `deny`, `read`, and `full` tiers, and how to tighten them
Report vulnerabilities privately via [GitHub security advisories](https://github.com/karnstack/reins/security/advisories/new).
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index e78833e..5bef114 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -4,7 +4,7 @@
### Minor Changes
-- 65ce7f3: `reins audit` — a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk.
+- 65ce7f3: `reins audit`, a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk.
## 0.3.0
@@ -19,7 +19,7 @@
### Patch Changes
-- 6885954: Trust the published Chrome Web Store extension (`hnjcfgochepemjndccfblpmfmlblkofo`) out of the box — store installs connect without `reins allow`.
+- 6885954: Trust the published Chrome Web Store extension (`hnjcfgochepemjndccfblpmfmlblkofo`) out of the box. Store installs connect without `reins allow`.
## 0.2.0
@@ -27,7 +27,7 @@
- f6b30a4: New `reins extension` command: install the extension without the Chrome Web
Store. The npm package now bundles the extension build with a key-pinned,
- pre-allowlisted id — `reins extension` stages it at `~/.reins/extension` for
+ pre-allowlisted id. `reins extension` stages it at `~/.reins/extension` for
Chrome's Load unpacked, no `reins allow` step. See docs/SIDELOAD.md.
### Patch Changes
diff --git a/packages/cli/src/bridge.ts b/packages/cli/src/bridge.ts
index 000f6d4..56546a9 100644
--- a/packages/cli/src/bridge.ts
+++ b/packages/cli/src/bridge.ts
@@ -232,14 +232,14 @@ export class BridgeHost implements BridgePort {
const entry = this.#browsers.get(browserId);
if (!entry || entry.ws.readyState !== WebSocket.OPEN) {
throw new Error(
- `unknown browserId "${browserId}"${roster ? ` — connected: ${roster}` : " — no browsers connected"}`,
+ `unknown browserId "${browserId}"${roster ? `. Connected: ${roster}` : ". No browsers connected"}`,
);
}
return { id: browserId, ws: entry.ws };
}
if (live.length === 0) throw new Error("extension not connected");
if (live.length > 1) {
- throw new Error(`several browsers connected — pass browserId. Connected: ${roster}`);
+ throw new Error(`several browsers connected. Pass browserId. Connected: ${roster}`);
}
const only = live[0] as BrowserInfo;
const entry = this.#browsers.get(only.id) as ConnectedBrowser;
diff --git a/packages/cli/src/integration.test.ts b/packages/cli/src/integration.test.ts
index 2105c53..8626dc7 100644
--- a/packages/cli/src/integration.test.ts
+++ b/packages/cli/src/integration.test.ts
@@ -78,7 +78,7 @@ function standInExtension(port: number): Promise {
error: {
code: "policy_denied",
message:
- "blocked by policy: x.com is read-only — grant full access from the reins extension popup",
+ "blocked by policy: x.com is read-only. Grant full access from the reins extension popup",
},
}),
);
diff --git a/packages/cli/src/rpc.test.ts b/packages/cli/src/rpc.test.ts
index 73a211e..981c023 100644
--- a/packages/cli/src/rpc.test.ts
+++ b/packages/cli/src/rpc.test.ts
@@ -90,7 +90,7 @@ describe("listAllTabs", () => {
it("errors on an unknown browserId, naming the roster", async () => {
const bridge = fakeBridge();
await expect(listAllTabs(bridge, "b9")).rejects.toThrow(
- 'unknown browserId "b9" — connected: b1 (Chrome)',
+ 'unknown browserId "b9". Connected: b1 (Chrome)',
);
});
});
diff --git a/packages/cli/src/rpc.ts b/packages/cli/src/rpc.ts
index 29c1fb4..2ecfe99 100644
--- a/packages/cli/src/rpc.ts
+++ b/packages/cli/src/rpc.ts
@@ -14,7 +14,7 @@ export async function listAllTabs(bridge: BridgePort, browserId?: string): Promi
const targets = browserId ? bridge.browsers.filter((b) => b.id === browserId) : bridge.browsers;
if (browserId !== undefined && targets.length === 0) {
const roster = bridge.browsers.map((b) => `${b.id} (${b.browser})`).join(", ");
- throw new Error(`unknown browserId "${browserId}"${roster ? ` — connected: ${roster}` : ""}`);
+ throw new Error(`unknown browserId "${browserId}"${roster ? `. Connected: ${roster}` : ""}`);
}
const results = await Promise.all(
targets.map(async (b) => {
diff --git a/packages/extension/CHANGELOG.md b/packages/extension/CHANGELOG.md
index 4dc24fd..7f22beb 100644
--- a/packages/extension/CHANGELOG.md
+++ b/packages/extension/CHANGELOG.md
@@ -4,7 +4,7 @@
### Minor Changes
-- 65ce7f3: `reins audit` — a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk.
+- 65ce7f3: `reins audit`, a per-action audit trail. The extension stamps each response with the resolved host, permission tier, and tab; the daemon writes one redacted JSONL line per action (policy denials included) to `~/.reins/logs/audit-YYYY-MM-DD.jsonl`, pruned after 30 days. Value-bearing params (typed text, fill values, eval code, CDP payloads) are redacted before anything reaches disk.
## 0.3.0
@@ -21,5 +21,5 @@
- f6b30a4: New `reins extension` command: install the extension without the Chrome Web
Store. The npm package now bundles the extension build with a key-pinned,
- pre-allowlisted id — `reins extension` stages it at `~/.reins/extension` for
+ pre-allowlisted id. `reins extension` stages it at `~/.reins/extension` for
Chrome's Load unpacked, no `reins allow` step. See docs/SIDELOAD.md.
diff --git a/packages/extension/src/lib/policy.ts b/packages/extension/src/lib/policy.ts
index a5bf7e4..21f83fd 100644
--- a/packages/extension/src/lib/policy.ts
+++ b/packages/extension/src/lib/policy.ts
@@ -82,7 +82,7 @@ export function tightenPolicy(patternInput: string, tier: Tier): Promise
existing?.tier ?? effectiveTier(p, pattern.startsWith("*.") ? pattern.slice(2) : pattern);
if (!tighterThan(tier, current)) {
throw new PolicyDenied(
- `policy_tighten can only restrict: "${pattern}" is already ${current} — grants require the extension popup`,
+ `policy_tighten can only restrict: "${pattern}" is already ${current}. Grants require the extension popup`,
);
}
const rules = existing
@@ -134,8 +134,8 @@ export async function ensureAllowed(method: GatedMethod, host: string | undefine
const label = host ?? "this tab";
const err = new PolicyDenied(
tier === "deny"
- ? `blocked by policy: ${label} is denied — change its tier from the reins extension popup`
- : `blocked by policy: ${label} is read-only — grant full access from the reins extension popup`,
+ ? `blocked by policy: ${label} is denied. Change its tier from the reins extension popup`
+ : `blocked by policy: ${label} is read-only. Grant full access from the reins extension popup`,
);
err.meta = { host, tier };
throw err;
diff --git a/packages/web/package.json b/packages/web/package.json
index 0fbba31..99b2072 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -5,7 +5,8 @@
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
- "build": "vite build && pagefind --site dist/client",
+ "build": "vite build && pagefind --site dist/client && node scripts/check-prose.mjs",
+ "check:prose": "node scripts/check-prose.mjs",
"preview": "vite preview",
"typecheck": "tsc --noEmit",
"deploy": "wrangler deploy"
diff --git a/packages/web/public/404.html b/packages/web/public/404.html
index 61bc559..e41e451 100644
--- a/packages/web/public/404.html
+++ b/packages/web/public/404.html
@@ -4,7 +4,7 @@
- Page not found — reins
+ Page not found · reins