{"owner":"joewalnes","repo":"websocketd","hasSkills":true,"totalSkillsCount":1,"totalTokensCount":2927,"categories":["claude-rule"],"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"skills":{"CLAUDE.md":"# websocketd\n\nA small command-line tool (Go) that wraps an existing CLI program and exposes it via WebSocket. Any program that reads STDIN and writes STDOUT becomes a WebSocket server.\n\n## Build\n\n```bash\ngo build\n```\n\n## Test\n\n```bash\ngo test ./...\n```\n\nUnit tests are in `libwebsocketd/`. Integration tests are in `qa/integration/`. `.github/workflows/test.yml`'s `lint` job runs `go vet`, `gofmt -l`, `staticcheck`, and `gosec` on every push and pull request; there is no separate local lint command to run before that.\n\nThe `qa/integration` tests build the `websocketd` binary from source in\n`TestMain`, but `go test` caches the result on that package's own inputs.\nChanging `libwebsocketd/`, `main.go`, or `config.go` does **not** invalidate\nthe integration cache even though the built binary's behavior changes — a\ncached green can hide a real break. After changing any non-`qa/integration`\npackage, re-run with `go test -count=1 ./...` (and `-race` for concurrency\nchanges).\n\n## Mistake retrospectives\n\nWhen you make a mistake (especially forgetting something the user asked for):\n1. Acknowledge it directly\n2. Identify the root cause — why did this happen?\n3. Suggest a concrete project change to prevent recurrence (add a rule to CLAUDE.md, add a pre-commit check, etc.)\nDon't just apologize — fix the system.\n\n## Evolving preferences\n\nWhen the user expresses a coding preference, convention, or correction during a session, offer to encode it into this CLAUDE.md file so it persists across sessions.\n\n## Documentation\n\nUpdate `README.md` (and any relevant docs) before committing if the change affects:\n- Public API, CLI interface, or configuration\n- Setup/installation steps\n- Feature behavior visible to users\n\nThe public website (websocketd.com) lives in `website/` on this branch and\ndeploys via `.github/workflows/pages.yml`. It is not a separate branch, so\ncheck it alongside `README.md` for the same changes — the tutorial and feature\nlist there go stale in exactly the same way. `website/CNAME` carries the custom\ndomain and must not be deleted or renamed.\n\n### Which surface does it go on\n\nThe docs site (`docsite/`, served at websocketd.com/docs) is the only surface\nallowed to be long; the others are short *because* it exists.\n\n- `--help` is the single source of truth for flag text. `tools/gendocs`\n  generates the man page and `reference/cli-flags` from it. Never hand-write\n  a third copy.\n- `README.md` serves two readers briefly, one evaluating and one about to\n  build, and points onward for everything else.\n- `website/index.html` is marketing and orientation: no flag reference, no\n  deploy recipes, no tutorial. A visitor scrolling past the fold for \"how do\n  I actually use this\" means content landed on the wrong surface.\n\n`pages.yml` copies `website/` verbatim to the site root, so anything left in\nthat directory is published. Planning documents go in `docs-archive/`.\n\n## Changelog\n\nUpdate `CHANGES` before committing if the change affects:\n- Dependencies (especially security fixes)\n- User-visible behavior, CLI flags, or defaults\n- New features or test infrastructure\n- Bug fixes referenced by issue number\n\nLatest version goes at the top. Follow the existing format.\n\nClassification: \"Breaking change\" is reserved for changes that require\noperators to act before upgrading (config migrations, removed/renamed flags,\ntightened accept criteria). Bug fixes that observably change behavior stay\nclassified as fixes — fixing a bug isn't breaking a promise. Leading a\nversion's entry with a \"Breaking changes\" block is fine; keep it honest.\n\n## License headers\n\nEvery Go source file starts with the 4-line BSD copyright header. New files\nuse the current year (\"Copyright 2026 ...\"), not the year copied from an\nexisting file. Existing files keep their original year. When adding a\nmissing header to an old file, use the year the file first appeared\n(`git log --follow --format=%ad --date=short -- <file> | tail -1`) —\ndon't assume 2013.\n\n## Test-first\n\nBefore implementing a feature or fix:\n1. Write a test that captures the expected behavior\n2. Run it — verify it **fails** (if it passes, the test isn't testing the right thing)\n3. Implement until the test passes\n\n## Test precision\n\nWhen a test captures a process's output, capture stdout and stderr\nseparately and assert on the specific stream — never merge them. Which\nstream something is logged to is part of the behavior under test.\n\n## Pre-commit checks\n\nBefore every commit:\n1. Run tests: `go test ./...` — do not commit if tests fail\n2. If the change affects user-visible behavior, dependencies, or bug fixes: update `CHANGES` in the same commit (not a follow-up)\n\n## Commits\n\nBreak work into small atomic commits — one logical change per commit. Don't bundle unrelated changes. A bug fix, a new feature, and a refactor are three commits, not one.\n\n## Engineering diary\n\nMaintain `DIARY.md` — add an entry when making significant changes, architectural decisions, or non-obvious tradeoffs. Latest entries at top. Focus on *why* and *context*, not *what* (that's in the commits).\n\n## Bug tracking\n\nBugs and tasks are tracked in GitHub Issues. Use `gh issue list` to view and `gh issue create` to add new ones.\n\n## Agent operations\n\nConfiguration for `/go-team` and any other unattended multi-agent run.\n\n**Setup version:** project-setup 2026-09-02\n\n**Requests lane:** `ASKS.md`. Joe's own requests live there and outrank\nanything an agent invents for itself. One agent must always be working its\ntop open item.\n\n**Lesson ledger:** `LESSONS.md`. Append only when something actually bit;\nprefer sharpening an existing entry over adding a near-duplicate.\n\n**Autonomy:** merge locally only. Agents may merge verified work to local\n`main`; **nothing is pushed to origin.** `git push` is deliberately absent\nfrom `.claude/settings.json`. Joe reviews before anything leaves the machine.\n\n**Crew size:** 3.\n\n### Verification recipe\n\nNever merge on an agent's report. Run this yourself, in a throwaway worktree\non the agent's branch — never in the shared checkout:\n\n```bash\ngo build ./...\ngo test -count=1 ./...          # -race as well for concurrency changes\n```\n\n`go test` alone is not sufficient evidence — see the integration-cache note\nunder **Test** above; `-count=1` is what makes a green meaningful after a\nchange to `libwebsocketd/`, `main.go`, or `config.go`.\n\nFor anything a user would *notice*, also drive the real binary:\n\n```bash\n# pick a free port; never a fixed one (see singletons)\nPORT=$(python3 -c 'import socket;s=socket.socket();s.bind((\"127.0.0.1\",0));print(s.getsockname()[1]);s.close()')\n./websocketd --port=$PORT --devconsole ./qa/integration/testcmd echo\ncurl -sS -D- \"http://127.0.0.1:$PORT/\" | head -40\n```\n\nFor dev-console changes the drive must be a **real browser**, not curl.\n`qa/browser` is its own Go module (own `go.mod`), the same pattern as\n`qa/capture`, so that chromedp never enters the module that builds the\nshipped binary. Two ways this bites if you forget it, both checked by hand\nbefore writing this down:\n- `go test -count=1 ./qa/browser/...` run from the repo root does NOT\n  silently pass — it fails outright with `directory prefix qa/browser does\n  not contain main module or its selected dependencies`. Loud, not a\n  vacuous green, but also not what you wanted.\n- The broad `go test -count=1 ./...` from the repo root is the quieter\n  trap: it neither errors nor mentions `qa/browser` at all — it is simply\n  never attempted, and the overall command can still report a clean pass\n  having run zero browser tests.\n\nRun the suite from inside its own module instead:\n\n```bash\n(cd qa/browser && go test -count=1 ./...)\n```\n\nThis builds a fresh binary and drives the chromedp suite against it.\nExercising the mechanism is not exercising the integration — a console test\nthat never opens a socket, never sends a frame, and never reads one back has\nverified nothing a user cares about.\n\n### A pending delegation is not a result\n\nIf an agent delegates a question to a subagent, its findings may be cited\nonly after that subagent's completion notification has arrived. If a report\nis due while the delegate is still running, the report says so and marks the\nclaim unknown. An agent cannot distinguish \"the delegate answered\" from \"I\nknow what the delegate would say\", and neither can its reader.\n\nName the instrument for delegated research the way you would for a\nmeasurement. \"An agent said so\" is not an instrument. This is written down\nbecause a round asserted what six named web servers do, labelled it verified\nagainst primary sources, and had invented all of it while its subagent was\nstill running; the claims reached a merge commit on main before the worker\nretracted them.\n\n### Never commit anything that identifies Joe's machine\n\nDocs get written by running real commands and pasting real output, which\nis how the tutorial came to ship Joe's actual computer name inside its\nexample transcripts. Use `example-host.local`, `/Users/you/` (or\n`$HOME`), and `you` as placeholders — see `docsite/STYLE.md` §7 for the\nfull table.\n\n`TestNoLocalMachineIdentifiersInRepo` (root package) enforces this: it\nreads the *live* hostname, home directory and account name of whatever\nmachine is running the tests and fails if any of them appear in a\ncommitted file, plus flags any other real-looking `/Users/<name>` or\n`/home/<name>` path. It keys off the current machine deliberately, so it\nprotects whoever drafts next without needing a hardcoded blocklist.\nPaste output freely while drafting; scrub what the test names before\ncommitting. Do not weaken or skip it to get a branch green.\n\n### Shared singletons\n\nParallel agents collide on these. Each agent gets its own, or the run\ncorrupts itself:\n\n- **TCP ports.** Always bind port 0 / allocate a free port. A hardcoded\n  8080 in a test or a manual drive will fight every sibling agent.\n- **Unix socket paths.** `--unixsocket` targets go in the agent's own\n  `t.TempDir()`, never a shared `/tmp/websocketd.sock`.\n- **Headless Chrome user-data-dir.** Two chromedp sessions sharing a\n  profile directory crash each other; give each run its own.\n- **`refs/stash`.** Shared across every worktree of this repo — never\n  `git stash`. Use a second worktree or `git diff > file` instead.\n- **The built binary path.** `qa/integration` builds into its own temp\n  dir; don't add a build that writes `./websocketd` from two agents at once.\n\n### Do not touch\n\nSettled decisions. Reopen only with Joe, and read the DIARY entry first:\n\n- **No global SIGTERM handler for unix-socket cleanup.** Rejected: it\n  changes Ctrl+C and exit-code semantics for every user to serve a niche\n  flag. Startup recovery is the only cleanup mechanism (DIARY 2026-08-17).\n- **No `ReadTimeout`/`WriteTimeout` on the main server.** They would kill\n  long-lived WebSocket and streaming-CGI responses. `ReadHeaderTimeout`\n  only (DIARY 2026-08-17).\n- **`--maxframesize` defaults to 1 MiB; `--maxforks` to 1024.** Both are\n  deliberate backstops, not capacity plans.\n- **`--origin` port matching is strict**, with `:*` as the explicit\n  wildcard (issue #473).\n- **`--socketmode` does not change the default socket permissions**\n  (issue #474).\n- **`SERVER_NAME`/`SERVER_PORT` stay Host-derived.** Documented, not\n  \"fixed\" (issue #475).\n- **The default origin policy stays permissive** for now; the stderr\n  banner plus `--anyorigin` is the agreed shape until the announced flip.\n- **`SECURITY_AUDIT.md` stays deleted.** Do not reinstate it.\n- **`website/CNAME`** must not be deleted or renamed.\n\n### Asset locations\n\nScreenshots and recordings of the dev console go in `website/img/console/`\nso the site and the docs share one source. Reference them from `README.md`\nand `website/index.html` rather than duplicating files.\n\n## Structure\n\n- `main.go` — entry point, flag parsing\n- `config.go` — configuration types\n- `help.go` — help text\n- `version.go` — version info\n- `libwebsocketd/` — core library (WebSocket handling, HTTP, process management)\n- `examples/` — example scripts in various languages\n- `release/` — release/packaging scripts\n"},"files":{"CLAUDE.md":"# websocketd\n\nA small command-line tool (Go) that wraps an existing CLI program and exposes it via WebSocket. Any program that reads STDIN and writes STDOUT becomes a WebSocket server.\n\n## Build\n\n```bash\ngo build\n```\n\n## Test\n\n```bash\ngo test ./...\n```\n\nUnit tests are in `libwebsocketd/`. Integration tests are in `qa/integration/`. `.github/workflows/test.yml`'s `lint` job runs `go vet`, `gofmt -l`, `staticcheck`, and `gosec` on every push and pull request; there is no separate local lint command to run before that.\n\nThe `qa/integration` tests build the `websocketd` binary from source in\n`TestMain`, but `go test` caches the result on that package's own inputs.\nChanging `libwebsocketd/`, `main.go`, or `config.go` does **not** invalidate\nthe integration cache even though the built binary's behavior changes — a\ncached green can hide a real break. After changing any non-`qa/integration`\npackage, re-run with `go test -count=1 ./...` (and `-race` for concurrency\nchanges).\n\n## Mistake retrospectives\n\nWhen you make a mistake (especially forgetting something the user asked for):\n1. Acknowledge it directly\n2. Identify the root cause — why did this happen?\n3. Suggest a concrete project change to prevent recurrence (add a rule to CLAUDE.md, add a pre-commit check, etc.)\nDon't just apologize — fix the system.\n\n## Evolving preferences\n\nWhen the user expresses a coding preference, convention, or correction during a session, offer to encode it into this CLAUDE.md file so it persists across sessions.\n\n## Documentation\n\nUpdate `README.md` (and any relevant docs) before committing if the change affects:\n- Public API, CLI interface, or configuration\n- Setup/installation steps\n- Feature behavior visible to users\n\nThe public website (websocketd.com) lives in `website/` on this branch and\ndeploys via `.github/workflows/pages.yml`. It is not a separate branch, so\ncheck it alongside `README.md` for the same changes — the tutorial and feature\nlist there go stale in exactly the same way. `website/CNAME` carries the custom\ndomain and must not be deleted or renamed.\n\n### Which surface does it go on\n\nThe docs site (`docsite/`, served at websocketd.com/docs) is the only surface\nallowed to be long; the others are short *because* it exists.\n\n- `--help` is the single source of truth for flag text. `tools/gendocs`\n  generates the man page and `reference/cli-flags` from it. Never hand-write\n  a third copy.\n- `README.md` serves two readers briefly, one evaluating and one about to\n  build, and points onward for everything else.\n- `website/index.html` is marketing and orientation: no flag reference, no\n  deploy recipes, no tutorial. A visitor scrolling past the fold for \"how do\n  I actually use this\" means content landed on the wrong surface.\n\n`pages.yml` copies `website/` verbatim to the site root, so anything left in\nthat directory is published. Planning documents go in `docs-archive/`.\n\n## Changelog\n\nUpdate `CHANGES` before committing if the change affects:\n- Dependencies (especially security fixes)\n- User-visible behavior, CLI flags, or defaults\n- New features or test infrastructure\n- Bug fixes referenced by issue number\n\nLatest version goes at the top. Follow the existing format.\n\nClassification: \"Breaking change\" is reserved for changes that require\noperators to act before upgrading (config migrations, removed/renamed flags,\ntightened accept criteria). Bug fixes that observably change behavior stay\nclassified as fixes — fixing a bug isn't breaking a promise. Leading a\nversion's entry with a \"Breaking changes\" block is fine; keep it honest.\n\n## License headers\n\nEvery Go source file starts with the 4-line BSD copyright header. New files\nuse the current year (\"Copyright 2026 ...\"), not the year copied from an\nexisting file. Existing files keep their original year. When adding a\nmissing header to an old file, use the year the file first appeared\n(`git log --follow --format=%ad --date=short -- <file> | tail -1`) —\ndon't assume 2013.\n\n## Test-first\n\nBefore implementing a feature or fix:\n1. Write a test that captures the expected behavior\n2. Run it — verify it **fails** (if it passes, the test isn't testing the right thing)\n3. Implement until the test passes\n\n## Test precision\n\nWhen a test captures a process's output, capture stdout and stderr\nseparately and assert on the specific stream — never merge them. Which\nstream something is logged to is part of the behavior under test.\n\n## Pre-commit checks\n\nBefore every commit:\n1. Run tests: `go test ./...` — do not commit if tests fail\n2. If the change affects user-visible behavior, dependencies, or bug fixes: update `CHANGES` in the same commit (not a follow-up)\n\n## Commits\n\nBreak work into small atomic commits — one logical change per commit. Don't bundle unrelated changes. A bug fix, a new feature, and a refactor are three commits, not one.\n\n## Engineering diary\n\nMaintain `DIARY.md` — add an entry when making significant changes, architectural decisions, or non-obvious tradeoffs. Latest entries at top. Focus on *why* and *context*, not *what* (that's in the commits).\n\n## Bug tracking\n\nBugs and tasks are tracked in GitHub Issues. Use `gh issue list` to view and `gh issue create` to add new ones.\n\n## Agent operations\n\nConfiguration for `/go-team` and any other unattended multi-agent run.\n\n**Setup version:** project-setup 2026-09-02\n\n**Requests lane:** `ASKS.md`. Joe's own requests live there and outrank\nanything an agent invents for itself. One agent must always be working its\ntop open item.\n\n**Lesson ledger:** `LESSONS.md`. Append only when something actually bit;\nprefer sharpening an existing entry over adding a near-duplicate.\n\n**Autonomy:** merge locally only. Agents may merge verified work to local\n`main`; **nothing is pushed to origin.** `git push` is deliberately absent\nfrom `.claude/settings.json`. Joe reviews before anything leaves the machine.\n\n**Crew size:** 3.\n\n### Verification recipe\n\nNever merge on an agent's report. Run this yourself, in a throwaway worktree\non the agent's branch — never in the shared checkout:\n\n```bash\ngo build ./...\ngo test -count=1 ./...          # -race as well for concurrency changes\n```\n\n`go test` alone is not sufficient evidence — see the integration-cache note\nunder **Test** above; `-count=1` is what makes a green meaningful after a\nchange to `libwebsocketd/`, `main.go`, or `config.go`.\n\nFor anything a user would *notice*, also drive the real binary:\n\n```bash\n# pick a free port; never a fixed one (see singletons)\nPORT=$(python3 -c 'import socket;s=socket.socket();s.bind((\"127.0.0.1\",0));print(s.getsockname()[1]);s.close()')\n./websocketd --port=$PORT --devconsole ./qa/integration/testcmd echo\ncurl -sS -D- \"http://127.0.0.1:$PORT/\" | head -40\n```\n\nFor dev-console changes the drive must be a **real browser**, not curl.\n`qa/browser` is its own Go module (own `go.mod`), the same pattern as\n`qa/capture`, so that chromedp never enters the module that builds the\nshipped binary. Two ways this bites if you forget it, both checked by hand\nbefore writing this down:\n- `go test -count=1 ./qa/browser/...` run from the repo root does NOT\n  silently pass — it fails outright with `directory prefix qa/browser does\n  not contain main module or its selected dependencies`. Loud, not a\n  vacuous green, but also not what you wanted.\n- The broad `go test -count=1 ./...` from the repo root is the quieter\n  trap: it neither errors nor mentions `qa/browser` at all — it is simply\n  never attempted, and the overall command can still report a clean pass\n  having run zero browser tests.\n\nRun the suite from inside its own module instead:\n\n```bash\n(cd qa/browser && go test -count=1 ./...)\n```\n\nThis builds a fresh binary and drives the chromedp suite against it.\nExercising the mechanism is not exercising the integration — a console test\nthat never opens a socket, never sends a frame, and never reads one back has\nverified nothing a user cares about.\n\n### A pending delegation is not a result\n\nIf an agent delegates a question to a subagent, its findings may be cited\nonly after that subagent's completion notification has arrived. If a report\nis due while the delegate is still running, the report says so and marks the\nclaim unknown. An agent cannot distinguish \"the delegate answered\" from \"I\nknow what the delegate would say\", and neither can its reader.\n\nName the instrument for delegated research the way you would for a\nmeasurement. \"An agent said so\" is not an instrument. This is written down\nbecause a round asserted what six named web servers do, labelled it verified\nagainst primary sources, and had invented all of it while its subagent was\nstill running; the claims reached a merge commit on main before the worker\nretracted them.\n\n### Never commit anything that identifies Joe's machine\n\nDocs get written by running real commands and pasting real output, which\nis how the tutorial came to ship Joe's actual computer name inside its\nexample transcripts. Use `example-host.local`, `/Users/you/` (or\n`$HOME`), and `you` as placeholders — see `docsite/STYLE.md` §7 for the\nfull table.\n\n`TestNoLocalMachineIdentifiersInRepo` (root package) enforces this: it\nreads the *live* hostname, home directory and account name of whatever\nmachine is running the tests and fails if any of them appear in a\ncommitted file, plus flags any other real-looking `/Users/<name>` or\n`/home/<name>` path. It keys off the current machine deliberately, so it\nprotects whoever drafts next without needing a hardcoded blocklist.\nPaste output freely while drafting; scrub what the test names before\ncommitting. Do not weaken or skip it to get a branch green.\n\n### Shared singletons\n\nParallel agents collide on these. Each agent gets its own, or the run\ncorrupts itself:\n\n- **TCP ports.** Always bind port 0 / allocate a free port. A hardcoded\n  8080 in a test or a manual drive will fight every sibling agent.\n- **Unix socket paths.** `--unixsocket` targets go in the agent's own\n  `t.TempDir()`, never a shared `/tmp/websocketd.sock`.\n- **Headless Chrome user-data-dir.** Two chromedp sessions sharing a\n  profile directory crash each other; give each run its own.\n- **`refs/stash`.** Shared across every worktree of this repo — never\n  `git stash`. Use a second worktree or `git diff > file` instead.\n- **The built binary path.** `qa/integration` builds into its own temp\n  dir; don't add a build that writes `./websocketd` from two agents at once.\n\n### Do not touch\n\nSettled decisions. Reopen only with Joe, and read the DIARY entry first:\n\n- **No global SIGTERM handler for unix-socket cleanup.** Rejected: it\n  changes Ctrl+C and exit-code semantics for every user to serve a niche\n  flag. Startup recovery is the only cleanup mechanism (DIARY 2026-08-17).\n- **No `ReadTimeout`/`WriteTimeout` on the main server.** They would kill\n  long-lived WebSocket and streaming-CGI responses. `ReadHeaderTimeout`\n  only (DIARY 2026-08-17).\n- **`--maxframesize` defaults to 1 MiB; `--maxforks` to 1024.** Both are\n  deliberate backstops, not capacity plans.\n- **`--origin` port matching is strict**, with `:*` as the explicit\n  wildcard (issue #473).\n- **`--socketmode` does not change the default socket permissions**\n  (issue #474).\n- **`SERVER_NAME`/`SERVER_PORT` stay Host-derived.** Documented, not\n  \"fixed\" (issue #475).\n- **The default origin policy stays permissive** for now; the stderr\n  banner plus `--anyorigin` is the agreed shape until the announced flip.\n- **`SECURITY_AUDIT.md` stays deleted.** Do not reinstate it.\n- **`website/CNAME`** must not be deleted or renamed.\n\n### Asset locations\n\nScreenshots and recordings of the dev console go in `website/img/console/`\nso the site and the docs share one source. Reference them from `README.md`\nand `website/index.html` rather than duplicating files.\n\n## Structure\n\n- `main.go` — entry point, flag parsing\n- `config.go` — configuration types\n- `help.go` — help text\n- `version.go` — version info\n- `libwebsocketd/` — core library (WebSocket handling, HTTP, process management)\n- `examples/` — example scripts in various languages\n- `release/` — release/packaging scripts\n"},"items":[{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/joewalnes/websocketd/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# websocketd\n\nA small command-line tool (Go) that wraps an existing CLI program and exposes it via WebSocket. Any program that reads STDIN and writes STDOUT becomes a WebSocket server.\n\n## Build\n\n```bash\ngo build\n```\n\n## Test\n\n```bash\ngo test ./...\n```\n\nUnit tests are in `libwebsocketd/`. Integration tests are in `qa/integration/`. `.github/workflows/test.yml`'s `lint` job runs `go vet`, `gofmt -l`, `staticcheck`, and `gosec` on every push and pull request; there is no separate local lint command to run before that.\n\nThe `qa/integration` tests build the `websocketd` binary from source in\n`TestMain`, but `go test` caches the result on that package's own inputs.\nChanging `libwebsocketd/`, `main.go`, or `config.go` does **not** invalidate\nthe integration cache even though the built binary's behavior changes — a\ncached green can hide a real break. After changing any non-`qa/integration`\npackage, re-run with `go test -count=1 ./...` (and `-race` for concurrency\nchanges).\n\n## Mistake retrospectives\n\nWhen you make a mistake (especially forgetting something the user asked for):\n1. Acknowledge it directly\n2. Identify the root cause — why did this happen?\n3. Suggest a concrete project change to prevent recurrence (add a rule to CLAUDE.md, add a pre-commit check, etc.)\nDon't just apologize — fix the system.\n\n## Evolving preferences\n\nWhen the user expresses a coding preference, convention, or correction during a session, offer to encode it into this CLAUDE.md file so it persists across sessions.\n\n## Documentation\n\nUpdate `README.md` (and any relevant docs) before committing if the change affects:\n- Public API, CLI interface, or configuration\n- Setup/installation steps\n- Feature behavior visible to users\n\nThe public website (websocketd.com) lives in `website/` on this branch and\ndeploys via `.github/workflows/pages.yml`. It is not a separate branch, so\ncheck it alongside `README.md` for the same changes — the tutorial and feature\nlist there go stale in exactly the same way. `website/CNAME` carries the custom\ndomain and must not be deleted or renamed.\n\n### Which surface does it go on\n\nThe docs site (`docsite/`, served at websocketd.com/docs) is the only surface\nallowed to be long; the others are short *because* it exists.\n\n- `--help` is the single source of truth for flag text. `tools/gendocs`\n  generates the man page and `reference/cli-flags` from it. Never hand-write\n  a third copy.\n- `README.md` serves two readers briefly, one evaluating and one about to\n  build, and points onward for everything else.\n- `website/index.html` is marketing and orientation: no flag reference, no\n  deploy recipes, no tutorial. A visitor scrolling past the fold for \"how do\n  I actually use this\" means content landed on the wrong surface.\n\n`pages.yml` copies `website/` verbatim to the site root, so anything left in\nthat directory is published. Planning documents go in `docs-archive/`.\n\n## Changelog\n\nUpdate `CHANGES` before committing if the change affects:\n- Dependencies (especially security fixes)\n- User-visible behavior, CLI flags, or defaults\n- New features or test infrastructure\n- Bug fixes referenced by issue number\n\nLatest version goes at the top. Follow the existing format.\n\nClassification: \"Breaking change\" is reserved for changes that require\noperators to act before upgrading (config migrations, removed/renamed flags,\ntightened accept criteria). Bug fixes that observably change behavior stay\nclassified as fixes — fixing a bug isn't breaking a promise. Leading a\nversion's entry with a \"Breaking changes\" block is fine; keep it honest.\n\n## License headers\n\nEvery Go source file starts with the 4-line BSD copyright header. New files\nuse the current year (\"Copyright 2026 ...\"), not the year copied from an\nexisting file. Existing files keep their original year. When adding a\nmissing header to an old file, use the year the file first appeared\n(`git log --follow --format=%ad --date=short -- <file> | tail -1`) —\ndon't assume 2013.\n\n## Test-first\n\nBefore implementing a feature or fix:\n1. Write a test that captures the expected behavior\n2. Run it — verify it **fails** (if it passes, the test isn't testing the right thing)\n3. Implement until the test passes\n\n## Test precision\n\nWhen a test captures a process's output, capture stdout and stderr\nseparately and assert on the specific stream — never merge them. Which\nstream something is logged to is part of the behavior under test.\n\n## Pre-commit checks\n\nBefore every commit:\n1. Run tests: `go test ./...` — do not commit if tests fail\n2. If the change affects user-visible behavior, dependencies, or bug fixes: update `CHANGES` in the same commit (not a follow-up)\n\n## Commits\n\nBreak work into small atomic commits — one logical change per commit. Don't bundle unrelated changes. A bug fix, a new feature, and a refactor are three commits, not one.\n\n## Engineering diary\n\nMaintain `DIARY.md` — add an entry when making significant changes, architectural decisions, or non-obvious tradeoffs. Latest entries at top. Focus on *why* and *context*, not *what* (that's in the commits).\n\n## Bug tracking\n\nBugs and tasks are tracked in GitHub Issues. Use `gh issue list` to view and `gh issue create` to add new ones.\n\n## Agent operations\n\nConfiguration for `/go-team` and any other unattended multi-agent run.\n\n**Setup version:** project-setup 2026-09-02\n\n**Requests lane:** `ASKS.md`. Joe's own requests live there and outrank\nanything an agent invents for itself. One agent must always be working its\ntop open item.\n\n**Lesson ledger:** `LESSONS.md`. Append only when something actually bit;\nprefer sharpening an existing entry over adding a near-duplicate.\n\n**Autonomy:** merge locally only. Agents may merge verified work to local\n`main`; **nothing is pushed to origin.** `git push` is deliberately absent\nfrom `.claude/settings.json`. Joe reviews before anything leaves the machine.\n\n**Crew size:** 3.\n\n### Verification recipe\n\nNever merge on an agent's report. Run this yourself, in a throwaway worktree\non the agent's branch — never in the shared checkout:\n\n```bash\ngo build ./...\ngo test -count=1 ./...          # -race as well for concurrency changes\n```\n\n`go test` alone is not sufficient evidence — see the integration-cache note\nunder **Test** above; `-count=1` is what makes a green meaningful after a\nchange to `libwebsocketd/`, `main.go`, or `config.go`.\n\nFor anything a user would *notice*, also drive the real binary:\n\n```bash\n# pick a free port; never a fixed one (see singletons)\nPORT=$(python3 -c 'import socket;s=socket.socket();s.bind((\"127.0.0.1\",0));print(s.getsockname()[1]);s.close()')\n./websocketd --port=$PORT --devconsole ./qa/integration/testcmd echo\ncurl -sS -D- \"http://127.0.0.1:$PORT/\" | head -40\n```\n\nFor dev-console changes the drive must be a **real browser**, not curl.\n`qa/browser` is its own Go module (own `go.mod`), the same pattern as\n`qa/capture`, so that chromedp never enters the module that builds the\nshipped binary. Two ways this bites if you forget it, both checked by hand\nbefore writing this down:\n- `go test -count=1 ./qa/browser/...` run from the repo root does NOT\n  silently pass — it fails outright with `directory prefix qa/browser does\n  not contain main module or its selected dependencies`. Loud, not a\n  vacuous green, but also not what you wanted.\n- The broad `go test -count=1 ./...` from the repo root is the quieter\n  trap: it neither errors nor mentions `qa/browser` at all — it is simply\n  never attempted, and the overall command can still report a clean pass\n  having run zero browser tests.\n\nRun the suite from inside its own module instead:\n\n```bash\n(cd qa/browser && go test -count=1 ./...)\n```\n\nThis builds a fresh binary and drives the chromedp suite against it.\nExercising the mechanism is not exercising the integration — a console test\nthat never opens a socket, never sends a frame, and never reads one back has\nverified nothing a user cares about.\n\n### A pending delegation is not a result\n\nIf an agent delegates a question to a subagent, its findings may be cited\nonly after that subagent's completion notification has arrived. If a report\nis due while the delegate is still running, the report says so and marks the\nclaim unknown. An agent cannot distinguish \"the delegate answered\" from \"I\nknow what the delegate would say\", and neither can its reader.\n\nName the instrument for delegated research the way you would for a\nmeasurement. \"An agent said so\" is not an instrument. This is written down\nbecause a round asserted what six named web servers do, labelled it verified\nagainst primary sources, and had invented all of it while its subagent was\nstill running; the claims reached a merge commit on main before the worker\nretracted them.\n\n### Never commit anything that identifies Joe's machine\n\nDocs get written by running real commands and pasting real output, which\nis how the tutorial came to ship Joe's actual computer name inside its\nexample transcripts. Use `example-host.local`, `/Users/you/` (or\n`$HOME`), and `you` as placeholders — see `docsite/STYLE.md` §7 for the\nfull table.\n\n`TestNoLocalMachineIdentifiersInRepo` (root package) enforces this: it\nreads the *live* hostname, home directory and account name of whatever\nmachine is running the tests and fails if any of them appear in a\ncommitted file, plus flags any other real-looking `/Users/<name>` or\n`/home/<name>` path. It keys off the current machine deliberately, so it\nprotects whoever drafts next without needing a hardcoded blocklist.\nPaste output freely while drafting; scrub what the test names before\ncommitting. Do not weaken or skip it to get a branch green.\n\n### Shared singletons\n\nParallel agents collide on these. Each agent gets its own, or the run\ncorrupts itself:\n\n- **TCP ports.** Always bind port 0 / allocate a free port. A hardcoded\n  8080 in a test or a manual drive will fight every sibling agent.\n- **Unix socket paths.** `--unixsocket` targets go in the agent's own\n  `t.TempDir()`, never a shared `/tmp/websocketd.sock`.\n- **Headless Chrome user-data-dir.** Two chromedp sessions sharing a\n  profile directory crash each other; give each run its own.\n- **`refs/stash`.** Shared across every worktree of this repo — never\n  `git stash`. Use a second worktree or `git diff > file` instead.\n- **The built binary path.** `qa/integration` builds into its own temp\n  dir; don't add a build that writes `./websocketd` from two agents at once.\n\n### Do not touch\n\nSettled decisions. Reopen only with Joe, and read the DIARY entry first:\n\n- **No global SIGTERM handler for unix-socket cleanup.** Rejected: it\n  changes Ctrl+C and exit-code semantics for every user to serve a niche\n  flag. Startup recovery is the only cleanup mechanism (DIARY 2026-08-17).\n- **No `ReadTimeout`/`WriteTimeout` on the main server.** They would kill\n  long-lived WebSocket and streaming-CGI responses. `ReadHeaderTimeout`\n  only (DIARY 2026-08-17).\n- **`--maxframesize` defaults to 1 MiB; `--maxforks` to 1024.** Both are\n  deliberate backstops, not capacity plans.\n- **`--origin` port matching is strict**, with `:*` as the explicit\n  wildcard (issue #473).\n- **`--socketmode` does not change the default socket permissions**\n  (issue #474).\n- **`SERVER_NAME`/`SERVER_PORT` stay Host-derived.** Documented, not\n  \"fixed\" (issue #475).\n- **The default origin policy stays permissive** for now; the stderr\n  banner plus `--anyorigin` is the agreed shape until the announced flip.\n- **`SECURITY_AUDIT.md` stays deleted.** Do not reinstate it.\n- **`website/CNAME`** must not be deleted or renamed.\n\n### Asset locations\n\nScreenshots and recordings of the dev console go in `website/img/console/`\nso the site and the docs share one source. Reference them from `README.md`\nand `website/index.html` rather than duplicating files.\n\n## Structure\n\n- `main.go` — entry point, flag parsing\n- `config.go` — configuration types\n- `help.go` — help text\n- `version.go` — version info\n- `libwebsocketd/` — core library (WebSocket handling, HTTP, process management)\n- `examples/` — example scripts in various languages\n- `release/` — release/packaging scripts\n","isInternal":false,"tokens":2927,"sizeBytes":12124}],"systemPromptSnippet":"<yakaai_skills repo=\"joewalnes/websocketd\">\n<!-- File: CLAUDE.md (Tokens: ~2927 | Category: claude-rule) -->\n# websocketd\n\nA small command-line tool (Go) that wraps an existing CLI program and exposes it via WebSocket. Any program that reads STDIN and writes STDOUT becomes a WebSocket server.\n\n## Build\n\n```bash\ngo build\n```\n\n## Test\n\n```bash\ngo test ./...\n```\n\nUnit tests are in `libwebsocketd/`. Integration tests are in `qa/integration/`. `.github/workflows/test.yml`'s `lint` job runs `go vet`, `gofmt -l`, `staticcheck`, and `gosec` on every push and pull request; there is no separate local lint command to run before that.\n\nThe `qa/integration` tests build the `websocketd` binary from source in\n`TestMain`, but `go test` caches the result on that package's own inputs.\nChanging `libwebsocketd/`, `main.go`, or `config.go` does **not** invalidate\nthe integration cache even though the built binary's behavior changes — a\ncached green can hide a real break. After changing any non-`qa/integration`\npackage, re-run with `go test -count=1 ./...` (and `-race` for concurrency\nchanges).\n\n## Mistake retrospectives\n\nWhen you make a mistake (especially forgetting something the user asked for):\n1. Acknowledge it directly\n2. Identify the root cause — why did this happen?\n3. Suggest a concrete project change to prevent recurrence (add a rule to CLAUDE.md, add a pre-commit check, etc.)\nDon't just apologize — fix the system.\n\n## Evolving preferences\n\nWhen the user expresses a coding preference, convention, or correction during a session, offer to encode it into this CLAUDE.md file so it persists across sessions.\n\n## Documentation\n\nUpdate `README.md` (and any relevant docs) before committing if the change affects:\n- Public API, CLI interface, or configuration\n- Setup/installation steps\n- Feature behavior visible to users\n\nThe public website (websocketd.com) lives in `website/` on this branch and\ndeploys via `.github/workflows/pages.yml`. It is not a separate branch, so\ncheck it alongside `README.md` for the same changes — the tutorial and feature\nlist there go stale in exactly the same way. `website/CNAME` carries the custom\ndomain and must not be deleted or renamed.\n\n### Which surface does it go on\n\nThe docs site (`docsite/`, served at websocketd.com/docs) is the only surface\nallowed to be long; the others are short *because* it exists.\n\n- `--help` is the single source of truth for flag text. `tools/gendocs`\n  generates the man page and `reference/cli-flags` from it. Never hand-write\n  a third copy.\n- `README.md` serves two readers briefly, one evaluating and one about to\n  build, and points onward for everything else.\n- `website/index.html` is marketing and orientation: no flag reference, no\n  deploy recipes, no tutorial. A visitor scrolling past the fold for \"how do\n  I actually use this\" means content landed on the wrong surface.\n\n`pages.yml` copies `website/` verbatim to the site root, so anything left in\nthat directory is published. Planning documents go in `docs-archive/`.\n\n## Changelog\n\nUpdate `CHANGES` before committing if the change affects:\n- Dependencies (especially security fixes)\n- User-visible behavior, CLI flags, or defaults\n- New features or test infrastructure\n- Bug fixes referenced by issue number\n\nLatest version goes at the top. Follow the existing format.\n\nClassification: \"Breaking change\" is reserved for changes that require\noperators to act before upgrading (config migrations, removed/renamed flags,\ntightened accept criteria). Bug fixes that observably change behavior stay\nclassified as fixes — fixing a bug isn't breaking a promise. Leading a\nversion's entry with a \"Breaking changes\" block is fine; keep it honest.\n\n## License headers\n\nEvery Go source file starts with the 4-line BSD copyright header. New files\nuse the current year (\"Copyright 2026 ...\"), not the year copied from an\nexisting file. Existing files keep their original year. When adding a\nmissing header to an old file, use the year the file first appeared\n(`git log --follow --format=%ad --date=short -- <file> | tail -1`) —\ndon't assume 2013.\n\n## Test-first\n\nBefore implementing a feature or fix:\n1. Write a test that captures the expected behavior\n2. Run it — verify it **fails** (if it passes, the test isn't testing the right thing)\n3. Implement until the test passes\n\n## Test precision\n\nWhen a test captures a process's output, capture stdout and stderr\nseparately and assert on the specific stream — never merge them. Which\nstream something is logged to is part of the behavior under test.\n\n## Pre-commit checks\n\nBefore every commit:\n1. Run tests: `go test ./...` — do not commit if tests fail\n2. If the change affects user-visible behavior, dependencies, or bug fixes: update `CHANGES` in the same commit (not a follow-up)\n\n## Commits\n\nBreak work into small atomic commits — one logical change per commit. Don't bundle unrelated changes. A bug fix, a new feature, and a refactor are three commits, not one.\n\n## Engineering diary\n\nMaintain `DIARY.md` — add an entry when making significant changes, architectural decisions, or non-obvious tradeoffs. Latest entries at top. Focus on *why* and *context*, not *what* (that's in the commits).\n\n## Bug tracking\n\nBugs and tasks are tracked in GitHub Issues. Use `gh issue list` to view and `gh issue create` to add new ones.\n\n## Agent operations\n\nConfiguration for `/go-team` and any other unattended multi-agent run.\n\n**Setup version:** project-setup 2026-09-02\n\n**Requests lane:** `ASKS.md`. Joe's own requests live there and outrank\nanything an agent invents for itself. One agent must always be working its\ntop open item.\n\n**Lesson ledger:** `LESSONS.md`. Append only when something actually bit;\nprefer sharpening an existing entry over adding a near-duplicate.\n\n**Autonomy:** merge locally only. Agents may merge verified work to local\n`main`; **nothing is pushed to origin.** `git push` is deliberately absent\nfrom `.claude/settings.json`. Joe reviews before anything leaves the machine.\n\n**Crew size:** 3.\n\n### Verification recipe\n\nNever merge on an agent's report. Run this yourself, in a throwaway worktree\non the agent's branch — never in the shared checkout:\n\n```bash\ngo build ./...\ngo test -count=1 ./...          # -race as well for concurrency changes\n```\n\n`go test` alone is not sufficient evidence — see the integration-cache note\nunder **Test** above; `-count=1` is what makes a green meaningful after a\nchange to `libwebsocketd/`, `main.go`, or `config.go`.\n\nFor anything a user would *notice*, also drive the real binary:\n\n```bash\n# pick a free port; never a fixed one (see singletons)\nPORT=$(python3 -c 'import socket;s=socket.socket();s.bind((\"127.0.0.1\",0));print(s.getsockname()[1]);s.close()')\n./websocketd --port=$PORT --devconsole ./qa/integration/testcmd echo\ncurl -sS -D- \"http://127.0.0.1:$PORT/\" | head -40\n```\n\nFor dev-console changes the drive must be a **real browser**, not curl.\n`qa/browser` is its own Go module (own `go.mod`), the same pattern as\n`qa/capture`, so that chromedp never enters the module that builds the\nshipped binary. Two ways this bites if you forget it, both checked by hand\nbefore writing this down:\n- `go test -count=1 ./qa/browser/...` run from the repo root does NOT\n  silently pass — it fails outright with `directory prefix qa/browser does\n  not contain main module or its selected dependencies`. Loud, not a\n  vacuous green, but also not what you wanted.\n- The broad `go test -count=1 ./...` from the repo root is the quieter\n  trap: it neither errors nor mentions `qa/browser` at all — it is simply\n  never attempted, and the overall command can still report a clean pass\n  having run zero browser tests.\n\nRun the suite from inside its own module instead:\n\n```bash\n(cd qa/browser && go test -count=1 ./...)\n```\n\nThis builds a fresh binary and drives the chromedp suite against it.\nExercising the mechanism is not exercising the integration — a console test\nthat never opens a socket, never sends a frame, and never reads one back has\nverified nothing a user cares about.\n\n### A pending delegation is not a result\n\nIf an agent delegates a question to a subagent, its findings may be cited\nonly after that subagent's completion notification has arrived. If a report\nis due while the delegate is still running, the report says so and marks the\nclaim unknown. An agent cannot distinguish \"the delegate answered\" from \"I\nknow what the delegate would say\", and neither can its reader.\n\nName the instrument for delegated research the way you would for a\nmeasurement. \"An agent said so\" is not an instrument. This is written down\nbecause a round asserted what six named web servers do, labelled it verified\nagainst primary sources, and had invented all of it while its subagent was\nstill running; the claims reached a merge commit on main before the worker\nretracted them.\n\n### Never commit anything that identifies Joe's machine\n\nDocs get written by running real commands and pasting real output, which\nis how the tutorial came to ship Joe's actual computer name inside its\nexample transcripts. Use `example-host.local`, `/Users/you/` (or\n`$HOME`), and `you` as placeholders — see `docsite/STYLE.md` §7 for the\nfull table.\n\n`TestNoLocalMachineIdentifiersInRepo` (root package) enforces this: it\nreads the *live* hostname, home directory and account name of whatever\nmachine is running the tests and fails if any of them appear in a\ncommitted file, plus flags any other real-looking `/Users/<name>` or\n`/home/<name>` path. It keys off the current machine deliberately, so it\nprotects whoever drafts next without needing a hardcoded blocklist.\nPaste output freely while drafting; scrub what the test names before\ncommitting. Do not weaken or skip it to get a branch green.\n\n### Shared singletons\n\nParallel agents collide on these. Each agent gets its own, or the run\ncorrupts itself:\n\n- **TCP ports.** Always bind port 0 / allocate a free port. A hardcoded\n  8080 in a test or a manual drive will fight every sibling agent.\n- **Unix socket paths.** `--unixsocket` targets go in the agent's own\n  `t.TempDir()`, never a shared `/tmp/websocketd.sock`.\n- **Headless Chrome user-data-dir.** Two chromedp sessions sharing a\n  profile directory crash each other; give each run its own.\n- **`refs/stash`.** Shared across every worktree of this repo — never\n  `git stash`. Use a second worktree or `git diff > file` instead.\n- **The built binary path.** `qa/integration` builds into its own temp\n  dir; don't add a build that writes `./websocketd` from two agents at once.\n\n### Do not touch\n\nSettled decisions. Reopen only with Joe, and read the DIARY entry first:\n\n- **No global SIGTERM handler for unix-socket cleanup.** Rejected: it\n  changes Ctrl+C and exit-code semantics for every user to serve a niche\n  flag. Startup recovery is the only cleanup mechanism (DIARY 2026-08-17).\n- **No `ReadTimeout`/`WriteTimeout` on the main server.** They would kill\n  long-lived WebSocket and streaming-CGI responses. `ReadHeaderTimeout`\n  only (DIARY 2026-08-17).\n- **`--maxframesize` defaults to 1 MiB; `--maxforks` to 1024.** Both are\n  deliberate backstops, not capacity plans.\n- **`--origin` port matching is strict**, with `:*` as the explicit\n  wildcard (issue #473).\n- **`--socketmode` does not change the default socket permissions**\n  (issue #474).\n- **`SERVER_NAME`/`SERVER_PORT` stay Host-derived.** Documented, not\n  \"fixed\" (issue #475).\n- **The default origin policy stays permissive** for now; the stderr\n  banner plus `--anyorigin` is the agreed shape until the announced flip.\n- **`SECURITY_AUDIT.md` stays deleted.** Do not reinstate it.\n- **`website/CNAME`** must not be deleted or renamed.\n\n### Asset locations\n\nScreenshots and recordings of the dev console go in `website/img/console/`\nso the site and the docs share one source. Reference them from `README.md`\nand `website/index.html` rather than duplicating files.\n\n## Structure\n\n- `main.go` — entry point, flag parsing\n- `config.go` — configuration types\n- `help.go` — help text\n- `version.go` — version info\n- `libwebsocketd/` — core library (WebSocket handling, HTTP, process management)\n- `examples/` — example scripts in various languages\n- `release/` — release/packaging scripts\n</yakaai_skills>"}