Skip to content

Make the agent instructions canonical in AGENTS.md - #127

Merged
juanmaguitar merged 1 commit into
trunkfrom
add-agents-md
Aug 6, 2026
Merged

Make the agent instructions canonical in AGENTS.md#127
juanmaguitar merged 1 commit into
trunkfrom
add-agents-md

Conversation

@juanmaguitar

Copy link
Copy Markdown
Collaborator

Replaces #126, which GitHub closed when its base branch was deleted on merging #125. Same content, retargeted at trunk.

Why

CLAUDE.md was the only place this project's constraints were written down, so every agent other than Claude Code started from nothing. That matters more here than in most repos: the premise — zero host prerequisites, no git binary, electron-store as the only persistence layer — is invisible from the code alone, and an agent that does not know it will reach for spawn('git') and be pleased with the result.

What changes

  • AGENTS.md is now canonical and tool-neutral. It carries what CLAUDE.md held, plus the directive to run the review before opening a pull request and summarise the outcome in the description.
  • CLAUDE.md shrinks to a pointer plus the one genuinely Claude Code specific line, so there is no second copy to drift.

No Copilot file

An earlier version of this added .github/copilot-instructions.md, because Copilot reads neither AGENTS.md nor CLAUDE.md. It is gone: #125 puts the review standard at .github/instructions/code-review.instructions.md, which Copilot reads natively, and that is the part it actually needs. Everything else here is context an agent can be pointed at.

Adding one anyway would have meant a second copy of the invariants with nothing keeping the two in step — the failure mode this PR exists to remove, reintroduced one file over.

Notes

AGENTS.md says plainly that nothing enforces the pre-PR review. It is a directive, and describing it as anything firmer would be false — worth keeping that way when the next person edits it.

No stubs for Codex or Cursor: both read AGENTS.md already, and per-tool files drift. Worth adding one when a tool is actually in use, not before.

Self-review

Documentation only, no JavaScript changed. npm run lint clean, 147 tests pass. No findings across the five dimensions.

CLAUDE.md was the only place the project's constraints were written down, so
every agent other than Claude Code started from nothing — on a codebase whose
whole premise (zero host prerequisites, no git binary, one persistence layer)
is invisible from the code alone.

AGENTS.md is now canonical and CLAUDE.md points at it, keeping only what is
genuinely Claude Code specific. AGENTS.md also carries the directive to run
the review before opening a pull request, and is explicit that nothing
enforces it.

No Copilot file: the review standard already sits at a path Copilot reads
natively, and everything else here is something it can be pointed at. Adding
one would have meant a second copy of the invariants with nothing keeping the
two in step.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar
juanmaguitar merged commit dac99e0 into trunk Aug 6, 2026
3 checks passed
@juanmaguitar
juanmaguitar deleted the add-agents-md branch August 6, 2026 08:11
juanmaguitar added a commit that referenced this pull request Aug 6, 2026
Closes #116.

`url:open` passed whatever it received to `shell.openExternal`, which
hands an address to whatever application the OS has registered for its
scheme — a wider action than "show this page in the browser". A `file:`
address opens an arbitrary local path in its associated application (on
Windows that can mean running it rather than viewing it), and any other
registered scheme is reachable the same way.

Every caller passes an http/https address today, so nothing misuses it.
It matters as the second half of a chain: the renderer displays content
the app does not author, and the URL the app auto-opens on server start
is parsed out of the Playground server's stdout. This guard is the step
that keeps any future influence over that string from becoming an action
on the contributor's machine.

## What changed

- **`src/external-url.js`** (new) — `ALLOWED_URL_SCHEMES = ['http:',
'https:']`, plus `isAllowedExternalUrl()` and `openExternalUrl()`. The
scheme is read off a parsed `new URL()` rather than the raw string, so
casing and leading whitespace (`FILE:`, ` file:`) normalize before the
comparison instead of being a way around it; an address Node can't parse
is refused rather than handed to the OS to interpret. What gets opened
is the parser's own `href`, not the caller's string. That second half
matters as much as the allow-list: the URL parser strips tabs and
newlines from anywhere in the input, including the middle of the scheme,
so `ht\ntp://example.com/x` validates as `http:` — and forwarding the
raw text would hand the OS an address nothing had checked. For every
caller here the two forms differ only by the trailing slash the parser
adds to a bare origin.
- **`src/main.js`** — the handler delegates to that module, passing
`shell.openExternal` and an `onRefused` that logs through the existing
`logEvent` (scope `url`). A refusal is visible in the log file people
attach to bug reports, not silent, so a future caller that trips the
guard is diagnosable. The logged address is truncated, since by
hypothesis it is the influenced input.

The logic lives in its own module, per the repo convention
(`src/bind-loopback.js`), so both sides of the guard are testable
without an Electron process.

No user-facing behaviour changes: every existing caller passes
http/https.

## Testing

`test/external-url.test.cjs` — 11 tests, using a stub in place of
`shell.openExternal` so "did this reach the OS?" is an assertion rather
than something the test takes on trust:

- The four address shapes the app actually passes (Trac, the feedback
form, the site on an ephemeral loopback port, wp-admin) still open, and
reach the stub in their normalized form.
- A control-character case (`ht\ntp://example.com/x`) pins the
normalization: it opens `http://example.com/x`, not the string carrying
the newline. This test fails against the first commit of this PR, which
forwarded the raw input.
- `file:///etc/passwd` and `file:///C:/Windows/System32/cmd.exe` never
reach the stub. The Windows one is the case that matters most: the OS
association for a `.exe` is "run it".
- `javascript:`, `data:`, `mailto:`, and OS-registered third-party
schemes (`ms-msdt:`, `vscode:`) are refused too.
- Junk input (`''`, whitespace, `null`, `undefined`, a number, an
object, an array, `'not a url'`) is refused rather than thrown.
- A refused address cannot forge a log line: control characters are
escaped rather than passed through, so an address carrying a newline
can't close its entry and open another one in the app's own
timestamp-and-scope format. Truncation runs after escaping, since
escaping is what decides the final length.
- One test pins the allow-list itself, so widening it has to be a
deliberate change that shows up in a diff.

Suite: **158 passing, 0 failing**, on `node --test` and again on
Electron's bundled Node via `npm run test:electron`. `npm run lint` is
clean across the repo.

### To verify by hand

Nothing in the UI can reach the refusal path by design, so the manual
pass is about confirming the allowed side is unaffected. On a signed
build from this branch:

1. Start a site — it should still auto-open in the browser as before.
2. Click the site URL and the **wp-admin** link in the site header —
both open.
3. Open a site with the database tool and click through to **adminer** —
opens.
4. Generate a patch and click the **core.trac.wordpress.org** link in
the "Next steps" text — opens.
5. Click the **feedback form** link — opens.
6. Optional, to see the guard fire: from the renderer devtools console,
run `window.api.openExternal('file:///etc/passwd')`. It should resolve
`false`, nothing should open, and the log file (Help → the log path)
should contain a `url` line reading `refused to open file:///etc/passwd
— only http:, https: are allowed`.

Also updates `.github/instructions/code-review.instructions.md`: it
named this handler as its known-open calibration example, which stops
being true here. Replaced with what the fix teaches, so the next review
looks for both halves rather than re-reporting something already closed.

Rebased on trunk after #122/#125/#127; the review standard's calibration
example moved with it to `.github/instructions/`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant