@drawbridge/drawbridge-agents 0.1.96 → 0.1.98
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -56,6 +56,15 @@
|
|
|
56
56
|
},
|
|
57
57
|
"hooks": {
|
|
58
58
|
"PreToolUse": [
|
|
59
|
+
{
|
|
60
|
+
"matcher": "Read|Bash",
|
|
61
|
+
"hooks": [
|
|
62
|
+
{
|
|
63
|
+
"type": "command",
|
|
64
|
+
"command": "node ./.claude/hooks/guard-secrets.js"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
59
68
|
{
|
|
60
69
|
"matcher": "Write|Edit|MultiEdit|NotebookEdit",
|
|
61
70
|
"hooks": [
|
|
@@ -13,8 +13,8 @@ The ten lines below are the release, as Darren confirmed them on 2026-10-22. The
|
|
|
13
13
|
2. The skill reads the task and subtasks, writes the stories, sends ONE numbered list and waits for the yes. The yes starts an unattended run.
|
|
14
14
|
3. Task → Building: branches, the release document, the band.
|
|
15
15
|
4. The skill builds on the release branches against the Local database, with the QA agent run locally. Nobody is asked anything; whatever needs a person is parked.
|
|
16
|
-
5. One end list: the parked questions, the access requests, the publish ask, the pull requests that are ready. The person answers, the skill closes out the rest, task → Review.
|
|
17
|
-
6. The developer reviews, runs the branch locally, reviews whenever ready and merges only when develop is free; runs any migration on dev; moves the task → QA and assigns the QA person.
|
|
16
|
+
5. One end list: the parked questions, the access requests, the publish ask, the steps that need the person's own phone or mailbox, the pull requests that are ready. The person answers and takes those steps one at a time, the skill walks and closes out the rest, and only when every walk has passed, task → Review.
|
|
17
|
+
6. The developer reviews, runs the branch locally, reviews whenever ready and merges only when develop is free; runs any migration on dev; moves the task → QA and assigns the QA person on the task and every subtask.
|
|
18
18
|
7. The QA person tests on dev.
|
|
19
19
|
8. The QA person moves the task → Approved. The developer cuts `release/<id>` from the tested develop commit and empties the band on develop, which frees develop for the next release.
|
|
20
20
|
9. The developer promotes `release/<id>` by hand ("Finish release", then the deploys), with the skill as checklist.
|
|
@@ -65,7 +65,7 @@ A run does not stop while there is code left to write. Whatever the hour, the ag
|
|
|
65
65
|
3. Copy `drawbridge-docs/templates/release.md` to `releases/<id>.md`, fill Scope and Branches (reviewer from line 2), commit on the docs branch. The markdown is the one record; there is no json.
|
|
66
66
|
4. Write this task's link into `drawbridge-api/release.json` and commit it on the api's `feature/<id>` branch ("release <id>: band"), never on `develop`: with several releases building at once, a band written to develop at open would name a release that is not on dev. The band reaches develop with the merge, and dev shows it above the account once the api redeploys, which is exactly when that release is the one on dev. A release that touches no api code still gets an api `feature/<id>` branch carrying this one commit and its own pull request at line 5. Nothing is committed to develop before the pull requests merge. The previous release's cut empties `release.json` on develop (line 8), so when this pull request merges after that, GitHub shows a conflict on `release.json` (changed on both sides): keep the branch's file.
|
|
67
67
|
5. **Every subtask links its story.** Append to each subtask's description, under its own text, one line per story: "Story: <story id>, <GitHub link>", the link being the story file in the docs repo on GitHub, `https://github.com/trydrawbridge/drawbridge-docs/blob/main/user-stories/<domain folder>/<file>.md` (`PUT /tasks/<subtask> { notes }`; the original description stays above, untouched). Never paste the story into the task: the file in drawbridge-docs is the one copy, and the link is read on GitHub (Darren, 2026-10-22: "link to the GitHub repo for each story in the subtask so it can be read in GitHub in the docs repo"; "Don't rewrite them in the task, just link to them"). The stories were pushed to docs `main` in step 2, so every link resolves the moment it is added; check each with `gh api repos/trydrawbridge/drawbridge-docs/contents/user-stories/<folder>/<file>.md` before writing it. The QA person cross-references the story against the feature before the final test.
|
|
68
|
-
6. Move every subtask from Application → Queue to Application → In progress (`POST /sections/1211315620607633/addTask { task }`). In the Application project, Queue means not scheduled and In progress means in a release (Darren, 2026-10-22); Complete comes at line 10. A code subtask carries no assignee while
|
|
68
|
+
6. Move every subtask from Application → Queue to Application → In progress (`POST /sections/1211315620607633/addTask { task }`). In the Application project, Queue means not scheduled and In progress means in a release (Darren, 2026-10-22); Complete comes at line 10. A code subtask carries no assignee while the release is Building: the assignee arrives with the section move, on the parent and every child together (the developer at line 5, the QA person at line 6), so clear an assignee left on a subtask from before the release (`PUT /tasks/<subtask> { assignee : null }`; Darren, 2026-10-28, on a subtask still assigned to him from September). A setup subtask the release needs a person for (switch a vendor on, buy and verify an SMS number) is the exception: it is assigned to the developer with a due date that leaves its lead time before the release's QA (a toll-free number takes about three business days to verify, so its due date is the Monday before the release's date, not the release's date). The release task itself lives in the Releases project only, never in Application or the Backlog, because a release draws its subtasks from both (Darren, 2026-10-22: "releases will include application tasks and backlog items, so their own board makes more sense"). A Backlog subtask keeps its Backlog membership; only Application subtasks get the Queue → In progress move.
|
|
69
69
|
7. **No pull request yet.** No "Marketing: What's new" subtask yet either: it is created, written and completed in one go at line 10, so developers and QA never see an open marketing item among the subtasks they are building and testing (Darren, 2026-10-22: "we should not have the marketing task in a release until it's completed, as it's confusing for the developers and qa people"). Nothing is opened, drafted or assigned at this point; the developer hears nothing until line 5, when the branch is finished. A pull request opened early reads to the developer as "review this now" and there is nothing to review (Darren, 2026-10-22, after the first run).
|
|
70
70
|
|
|
71
71
|
## Line 4: build, on Local
|
|
@@ -86,6 +86,7 @@ All work on the release branches against the **Local** database. Develop and its
|
|
|
86
86
|
6. **QA agent on Local.** Dispatch `drawbridge-qa` with `target : local`, once per subtask that has a story (story paths, the SUBTASK id, the organization id: `APP_TEST_ORG_ID` in `drawbridge-tests/.env`, the e2e organization `fixtures/e2e-org.md` describes, present on Local because Local is a copy of dev). A subtask spanning many stories gets one report listing them all. Fix every fail on the release branch, redispatch, until every `drawbridge-tests/results/qa/<subtask id>.json` reads `fails : 0`. The fixes are commits in the same pull request; that is why the agent runs here and not on dev (Darren: "all agent work is done locally so the work and fixes get committed as part of the PR").
|
|
87
87
|
**Inbound webhooks are walked against dev, by the agent.** Every Local Environment points `APP_CLIENT_WEBHOOKS_URI` at `http://localhost:4002`, which Shopify, Stripe and Twilio cannot reach, so the agent connects to dev for that one step instead of handing it on (Darren, 2026-10-28: "if you need to test a third party integration you might have to connect to the dev environment, but we must always do this"). The vendor is already pointed at dev's webhooks service (every number and store registers dev's URL), so the agent triggers the event there — a STOP text to dev's number, a test order on the dev store, a Stripe test event — and reads what dev wrote (the `buffer` row, the suppression row, the screen). A change to how the event is HANDLED downstream (sync, api) is then walked on Local by copying that dev `buffer` row into Local's `buffer` collection, where Local's sync drains it through the new code. A change to how it is RECEIVED (drawbridge-webhooks itself) cannot run anywhere but dev until it is deployed there, so the agent walks it on dev right after the developer's merge deploys (line 6), before the task moves to QA, and the developer hears from the agent before the tester does. Either way the walk is recorded in the QA table like any other; "webhooks: tested on dev" by the QA person is no longer a row.
|
|
88
88
|
**Nothing else is the exception.** A blocked scenario is a step nobody has run, and a developer never receives one (Darren, 2026-10-28: "I don't want testing with our developers happening on tasks that you haven't validated locally"). Local reaches more than it looks: it shares dev's Twilio, SendGrid, Spaces, Gemini and BrightData keys, so a text can be sent from a number bought on Local, a real bounce can be made by sending a winner email to an address that does not exist, the SendGrid API key on the sendgrid provider row lists and clears that bounce (`GET`/`DELETE /v3/suppression/bounces`; no dashboard login is needed), and a campaign can be generated from a site with a page video. Each of those is a paid run or a vendor action, so it is parked, asked for in the end list with what it unblocks, and walked on Local once granted, before the task moves to Review (Darren, 2026-10-28, granted all three for Cherry). A Twilio number bought on Local lives in the shared account: once sync marks its row `verified`, a developer copies that one identity row into dev's test organization and dev's sync marks it Active on its next sweep, so dev needs no second purchase and no second three-day verification. A second member of the e2e organization (for the read-only scenarios) needs no vendor: invite one on Local and accept from the connected mailbox. **A fail inside a path the release changed is a bug to find, never a note on the list**: Cherry's winner emails did not arrive on Local and the list told the tester to "check the winner email on dev" instead.
|
|
89
|
+
**The person who started the release is part of the walk when a step needs a person.** Some steps end on a phone or in a mailbox the agent cannot read: a text that must be received and replied to (STOP, START, HELP), a vendor's own auto-reply that shows only on the phone (Twilio answers HELP itself and logs no reply), a mail Gmail filed in Spam, which the mailbox connector cannot see, a caller id the shared Twilio trial account must hold before it will text anyone. Those steps are the release creator's, not a developer's or a tester's (Darren, 2026-10-28: "lets make sure that our skill ensures that all features are tested and walked and passed. If the release creator needs to be involved, make sure to do so"). They are parked like any other need and asked for in the end list, in order, one numbered item per step with what the person does and what they must send back (a phone number to add, "reply STOP now", "paste the reply word for word", "look in Spam for <subject>"). The agent hands each step over, waits for "done", reads the evidence itself (Twilio's message log, the `buffer` row, the suppression row, the step row, the screen) before it hands over the next, and writes the outcome into the story and the QA table. Cherry's SMS walk ran this way with Darren's phone on 2026-10-28; it took five hand-overs and settled the HELP sentence in two stories.
|
|
89
90
|
7. **Review.** `superpowers:code-reviewer` against the stories, then `ponytail-review`, on each repo's diff.
|
|
90
91
|
8. Push the release branches.
|
|
91
92
|
|
|
@@ -93,19 +94,21 @@ All work on the release branches against the **Local** database. Develop and its
|
|
|
93
94
|
|
|
94
95
|
Only when every subtask that can be built has been built: each committed, `npm test` green in each repo, every QA report at `fails : 0`, both review passes done, the branches pushed. A pull request is the request to review, so it exists only once there is something to review.
|
|
95
96
|
|
|
96
|
-
**First, the end list.** ONE numbered list to the person who started the release, the first words anyone has heard since the yes (Darren, 2026-10-22: "ask finalizing questions / request access to complete then"). In this order: every parked question, every access request with what it unblocks, the publish ask when there is a package, the subtasks not built and why, and the repos whose pull requests are ready to open. Each item three plain sentences (rule 7). Then, in the same message, open the pull requests that depend on nothing parked (steps 1 to 3 below) and stop. When the answers and grants arrive, finish the parked steps on the release branches, bump the pins once the registry has the version, run the tests and the QA agent on what changed, open the remaining pull requests, and only then move the task to Review. This close-out is the end of the coding segment of a release; nothing in it is asked for a second time.
|
|
97
|
+
**First, the end list.** ONE numbered list to the person who started the release, the first words anyone has heard since the yes (Darren, 2026-10-22: "ask finalizing questions / request access to complete then"). In this order: every parked question, every access request with what it unblocks, the publish ask when there is a package, the subtasks not built and why, and the repos whose pull requests are ready to open. Each item three plain sentences (rule 7). Then, in the same message, open the pull requests that depend on nothing parked (steps 1 to 3 below) and stop. When the answers and grants arrive, finish the parked steps on the release branches, bump the pins once the registry has the version, run the tests and the QA agent on what changed, open the remaining pull requests, and only then move the task to Review. This close-out is the end of the coding segment of a release; nothing in it is asked for a second time, except the step-by-step hand-overs a walk needs from the person (line 4 step 6), which were announced in the list and run in order.
|
|
98
|
+
|
|
99
|
+
**The gate to Review is every walk passed, none parked.** Before the task moves, the release doc's QA table reads one row per changed path with a pass, and no row reads blocked, parked, "tested on dev by QA" or "check on dev". A row that cannot pass yet is a reason the task stays in Building, not a note for the developer: the agent asks the release creator for what the walk needs (the grant, the phone, the reply) and walks it before moving on. Cherry moved to Review on 2026-10-28 with its SMS, Shopify and scrape walks still parked and had to be walked afterwards; the task should have stayed in Building until they passed.
|
|
97
100
|
|
|
98
101
|
1. One pull request per repo: `gh pr create --base develop --head feature/<id> --title "Release <id>: <task name>" --assignee <developer> --reviewer <developer> --body <the Scope table, the per-subtask commits, the QA reports' paths, and the migration's dev command if there is one>`. Components and utils target `main`.
|
|
99
102
|
2. Every pull request body ends with the lane rule in one line: "Review any time, on the feature branch locally. Merge only when develop is free: one release on develop at a time, so this merges once the release before it is approved and cut to its release/ branch." Fill the Branches table in `releases/<id>.md` with the PR links; commit.
|
|
100
|
-
3. **How to test, on every subtask, for both readers.** Append to each subtask's description, under its Story lines, two headings (Darren, 2026-10-22: "steps for a developer to test via code, or locally, and a QA person's steps"). First, "
|
|
101
|
-
4. Attach every QA report's screenshots to the release task (`node lib/asana-attach.js <release id> <png>` from drawbridge-tests, one per scenario, named `<subtask id>-<scenario>.png`), because the reports under `results/qa/` are gitignored and exist only on the machine that ran them. Then put the PR links on the release task as ONE numbered list (`drawbridge-asana-comment`): this is the first Asana comment of the release, and the package pull request's link rides in it too. Assign the task to the developer and move
|
|
103
|
+
3. **How to test, on every subtask, for both readers.** Append to each subtask's description, under its Story lines, two headings (Darren, 2026-10-22: "steps for a developer to test via code, or locally, and a QA person's steps"). First, "Review (developer)": the developer reads the code and makes sure it works; it is not a script of commands to run (Darren, 2026-10-28: "commands aren't what should be run to validate"). One numbered list: the pull request link, then one item per change saying what it does and the rule it has to keep (the test that proves it, named so the reviewer can read it), then the checks only a developer can make: the deployment reads ACTIVE, the build log is clean, no new Sentry issue, the migration's dry-run counts when there is one. No `npm test`, no walk command, no fixed organization id. Then, "Test on dev (QA)": one numbered list with the dev app to open first (dashboard `https://dev.dashboard.trydrawbridge.com/organizations`, then "Open an organization with the following features: <what this subtask needs>" and the screens by name from there, never a fixed organization id, a share page `https://<slug>.dev.drwbrdg.com/...`, the website `https://dev.trydrawbridge.com/...`, the components app `https://components.trydrawbridge.com/...`), then one step per check (what the tester does, then what they must see), fifteen steps at most (`drawbridge-asana-comment`; Darren, 2026-10-28, on a 52-step list: "It's insane to expect someone to go through this list"). **Only the paths the subtask changed.** A story's other scenarios (the Klaviyo sync behind an SMS change, the Shopify reconnect behind a campaign picker, the Google accounts behind a logging change) are not walked again for this release (Darren, 2026-10-28: "we don't need unchanged paths"); that is how a list lands under fifteen without folding steps. **Build and log steps are the developer's.** A Dockerfile, a lockfile, a DigitalOcean build log or deployment status, a Sentry issue, an api log line: these go on the developer's list only, never on the QA list (Darren, 2026-10-28: "all build and log steps should be developer only not QA"). **Every step on both lists was run on Local in line 4**, by the agent, with the access the end list obtained; a step that was not run is not written (line 4 step 6). Every label in it is quoted from the Local walk and the QA reports of line 4, never from memory (`conventions/walk-the-app.md`), with dev URLs in place of localhost. A migration subtask says what the tester checks after the developer's dev run. The words go through `humanizer`. Both lists are written here, after the build, because only then are the changes and labels known (Darren, 2026-10-22: "include steps on how to test in the respective development apps"). **The description is rewritten whole at this point**: the original text, the Story lines, the two lists, nothing else. Build notes, "still to do" lines and parked items live in the release document, and one left in a description reads as unfinished work to the developer (Cherry's sample products subtask carried a "Still to do in Cherry" block after all three items were done). The pull request body and the release doc's QA table point at the subtasks for these steps rather than repeating them.
|
|
104
|
+
4. Attach every QA report's screenshots to the release task (`node lib/asana-attach.js <release id> <png>` from drawbridge-tests, one per scenario, named `<subtask id>-<scenario>.png`), because the reports under `results/qa/` are gitignored and exist only on the machine that ran them. Then put the PR links on the release task as ONE numbered list (`drawbridge-asana-comment`): this is the first Asana comment of the release, and the package pull request's link rides in it too. Assign the task AND every subtask to the developer (`PUT /tasks/<id> { assignee }` on the parent and on each child, the setup subtasks included) and move the task to Review (`POST /sections/1219762451512990/addTask`) only once every pull request is open, parked items included. A section move always carries the assignee to the parent and all its children, so the person's Asana shows everything the release asks of them (Darren, 2026-10-28: "whoever is assigned as the developer is assigned the parent and child tasks when going to review, and QA gets all tasks when sent to that section").
|
|
102
105
|
5. Stop. Report the PR links.
|
|
103
106
|
|
|
104
107
|
## Lines 6 to 8: the developer and the QA person
|
|
105
108
|
|
|
106
109
|
The skill does none of this. For the record, so the skill can answer "what is the next step":
|
|
107
110
|
|
|
108
|
-
- Line 6, the developer: reviews the diff, runs the branch locally, approves and merges, but merges only when develop is free. Develop holds one release at a time: the review happens whenever the developer likes, on the feature branch locally, and the approved release waits in Review until the one before it is cut at line 8 (Darren, 2026-10-23: "Develop can only go one at a time"; "The Review slot may queue up once the devs have reviewed a release but that's fine"). Merging means the developer has QA'd the branch locally and it is ready (Darren, 2026-10-23). Then the merge, the dev migration, the move to QA and the QA assignment, in that order. Dev deploys itself. If there is a migration: `npm run migrate -- <id> --dry --env dev` from the api checkout on `develop`, read the printed result, then without `--dry`. The developer may have an agent run those two dev commands on their explicit instruction in their own session (Darren, 2026-10-22); the skill never runs them on its own. Then moves the task to QA and assigns the QA person in Asana.
|
|
111
|
+
- Line 6, the developer: reviews the diff, runs the branch locally, approves and merges, but merges only when develop is free. Develop holds one release at a time: the review happens whenever the developer likes, on the feature branch locally, and the approved release waits in Review until the one before it is cut at line 8 (Darren, 2026-10-23: "Develop can only go one at a time"; "The Review slot may queue up once the devs have reviewed a release but that's fine"). Merging means the developer has QA'd the branch locally and it is ready (Darren, 2026-10-23). Then the merge, the dev migration, the move to QA and the QA assignment, in that order. Dev deploys itself. If there is a migration: `npm run migrate -- <id> --dry --env dev` from the api checkout on `develop`, read the printed result, then without `--dry`. The developer may have an agent run those two dev commands on their explicit instruction in their own session (Darren, 2026-10-22); the skill never runs them on its own. Then moves the task to QA and assigns the QA person in Asana, on the parent and on every subtask (the same rule as the move to Review: a section move carries its assignee to the whole family of tasks).
|
|
109
112
|
- Line 7, the QA person: tests on dev. The band already names the release.
|
|
110
113
|
- Line 8, the QA person: moves the task to Approved. Then the developer cuts the release, below.
|
|
111
114
|
|
|
@@ -149,6 +152,7 @@ The cut stages the approved release for production and frees develop for the nex
|
|
|
149
152
|
- Write a release document, branch or migration under any name but the Asana id.
|
|
150
153
|
- Build or run the QA agent against dev or develop before the pull requests merge. Local only.
|
|
151
154
|
- Hand a developer or a tester a step the agent did not run on Local. A blocked scenario is parked and asked for, never written into a test list with a note (line 4 step 6).
|
|
155
|
+
- Move the task to Review while any walk is parked or blocked. A step that needs a person's phone, mailbox or account goes to the release creator, one hand-over at a time, and the task waits in Building until it passes (line 4 step 6, line 5).
|
|
152
156
|
- Put a build, lockfile, deployment, log or Sentry step on the QA list. Those are the developer's (line 5 step 3).
|
|
153
157
|
|
|
154
158
|
## Section ids
|
package/README.md
CHANGED
|
@@ -112,6 +112,9 @@ identical copy via the sync mirror (see "Delivery model").
|
|
|
112
112
|
doc anchor and on `@story`-anchored code changed without a drawbridge-docs change (once per
|
|
113
113
|
turn; the second attempt passes so the agent must answer), and **nudges** (non-blocking) when
|
|
114
114
|
the graph looks stale. It skips plan-mode and turns with no source changes.
|
|
115
|
+
- **Secrets guard.** `hooks/guard-secrets.js` (PreToolUse on Read/Bash) denies a Read of any
|
|
116
|
+
`.env*` file and a Bash command that displays one, prints the environment, or prints an `op`
|
|
117
|
+
value without a redirect. See `conventions/secrets.md`.
|
|
115
118
|
- **Read-before-write hooks.** `hooks/consult-docs-record.js` (PostToolUse on Read/Bash) remembers
|
|
116
119
|
that a drawbridge-docs page was read and a graph query ran in this session;
|
|
117
120
|
`hooks/consult-docs-gate.js` (PreToolUse on Write/Edit) refuses the first source edit until
|
package/claude/CLAUDE.md
CHANGED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Never put a secret in the transcript
|
|
2
|
+
|
|
3
|
+
Nothing an agent prints, reads or echoes may carry a secret. The family's env files
|
|
4
|
+
(`.env.local`, `.env.development`, `.env.production`, drawbridge-tests' `.env`) hold a 1Password
|
|
5
|
+
service-account token and, in tests, live keys. A transcript is copied, summarized and shared,
|
|
6
|
+
so a value that lands in one has to be rotated.
|
|
7
|
+
|
|
8
|
+
The api's 1Password service-account token reached a session transcript twice, the second time
|
|
9
|
+
on 2026-10-28. Darren: "That is the second time you have done that. You cannot fucking keep doing
|
|
10
|
+
that." This page and `hooks/guard-secrets.js` exist so there is no third time.
|
|
11
|
+
|
|
12
|
+
## The rule
|
|
13
|
+
|
|
14
|
+
- Never `Read`, `cat`, `head`, `tail`, `less`, `grep`, `sed`, `awk` or `source` a `.env*` file.
|
|
15
|
+
Not to "see which Environment it names", not to copy one line, not to check it exists (`ls` and
|
|
16
|
+
`test -f` are fine; they show no content).
|
|
17
|
+
- Never print `process.env`, `printenv`, bare `env`, or a value read from `op` (`op read`,
|
|
18
|
+
`op item get`, `op environment read`) to the console. Those commands are allowed only with
|
|
19
|
+
their output redirected to a file that nothing then displays.
|
|
20
|
+
- Never paste a key, token, connection string, cookie or session value into a reply, a commit
|
|
21
|
+
message, an Asana comment, a doc or a script's console output.
|
|
22
|
+
- A script that needs an env value runs under the repo's `scripts/op.sh`
|
|
23
|
+
(`ENV_FILE=.env.local sh scripts/op.sh node <script>`) and consumes the value inside the
|
|
24
|
+
process. `op run` conceals secret-looking strings in console output, which is why ids are
|
|
25
|
+
written to files and read back; it is not a licence to display the env file.
|
|
26
|
+
- Copying an env file between checkouts (a worktree without `.env.development`) is `cp` with no
|
|
27
|
+
display, and the copy is deleted when the work is done.
|
|
28
|
+
- If a secret does reach the transcript, say so at once, in one line, and name what to rotate.
|
|
29
|
+
|
|
30
|
+
## Enforced
|
|
31
|
+
|
|
32
|
+
`hooks/guard-secrets.js` is a PreToolUse hook on `Read` and `Bash`, mirrored into every repo's
|
|
33
|
+
`.claude/hooks/`. It denies a `Read` of any `.env*` file and a Bash command that displays one,
|
|
34
|
+
prints the environment, or prints an `op` value without a redirect. The denial names this page.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PreToolUse hook (Read|Bash): nothing displays a secret. The family's env files carry a
|
|
3
|
+
// 1Password service-account token, and that token reached a session transcript twice
|
|
4
|
+
// (the second time 2026-10-28). A convention did not stop it, so this does: a Read of any
|
|
5
|
+
// .env* file is denied, and so is a Bash command that displays one, prints the environment,
|
|
6
|
+
// or prints an `op` value to the console. See conventions/secrets.md.
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
|
|
10
|
+
const readStdin = () => {
|
|
11
|
+
try {
|
|
12
|
+
return JSON.parse(fs.readFileSync(0, 'utf8') || '{}')
|
|
13
|
+
} catch (error) {
|
|
14
|
+
return {}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const deny = (reason) => {
|
|
19
|
+
process.stdout.write(JSON.stringify({
|
|
20
|
+
hookSpecificOutput: {
|
|
21
|
+
hookEventName: 'PreToolUse',
|
|
22
|
+
permissionDecision: 'deny',
|
|
23
|
+
permissionDecisionReason: reason
|
|
24
|
+
}
|
|
25
|
+
}))
|
|
26
|
+
process.exit(0)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SEE = ' See conventions/secrets.md: run the script under scripts/op.sh and let it consume the value; never display it.'
|
|
30
|
+
|
|
31
|
+
// `.env`, `.env.local`, `.env.development`, `.env.production`, `.env.example` is harmless but
|
|
32
|
+
// treated the same: nothing needs to read any of them.
|
|
33
|
+
const isEnvFile = (filePath) => /^\.env(\..+)?$/.test(path.basename(filePath))
|
|
34
|
+
|
|
35
|
+
// A command whose argument list names an env file, by a tool that prints file content.
|
|
36
|
+
const DISPLAYS_FILE = /(^|[|;&(\s])(cat|head|tail|less|more|bat|grep|rg|egrep|fgrep|sed|awk|cut|sort|uniq|strings|xxd|od|source|\.)\s+(?:[^|;&]*[\s/'"=])?\.env(\.[\w-]+)?(['"\s]|$)/m
|
|
37
|
+
|
|
38
|
+
// The whole environment, printed.
|
|
39
|
+
const PRINTS_ENV = /(^|[|;&(\s])(printenv|env)(\s*$|\s*[|;&)]|\s+-)/m
|
|
40
|
+
|
|
41
|
+
// An `op` read whose output is not sent to a file.
|
|
42
|
+
const OP_READ = /(^|[|;&(\s])op\s+(read|item\s+get|environment\s+read)\b/m
|
|
43
|
+
|
|
44
|
+
const run = () => {
|
|
45
|
+
const input = readStdin()
|
|
46
|
+
const tool = input.tool_name || ''
|
|
47
|
+
const toolInput = input.tool_input || {}
|
|
48
|
+
|
|
49
|
+
if (tool === 'Read') {
|
|
50
|
+
const filePath = toolInput.file_path || ''
|
|
51
|
+
if (filePath && isEnvFile(filePath)) {
|
|
52
|
+
deny('Blocked: `' + path.basename(filePath) + '` holds a secret and is never read into the transcript.' + SEE)
|
|
53
|
+
}
|
|
54
|
+
process.exit(0)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (tool !== 'Bash') process.exit(0)
|
|
58
|
+
|
|
59
|
+
const command = String(toolInput.command || '')
|
|
60
|
+
if (!command) process.exit(0)
|
|
61
|
+
|
|
62
|
+
if (DISPLAYS_FILE.test(command)) {
|
|
63
|
+
deny('Blocked: this command displays a .env file, which holds a secret.' + SEE)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (PRINTS_ENV.test(command)) {
|
|
67
|
+
deny('Blocked: this command prints the whole environment, secrets included.' + SEE)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (OP_READ.test(command) && !/>\s*\S/.test(command)) {
|
|
71
|
+
deny('Blocked: an `op` read prints a secret to the console. Redirect it to a file nothing displays, or run the consumer under scripts/op.sh.' + SEE)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
process.exit(0)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
run()
|
|
79
|
+
} catch (error) {
|
|
80
|
+
// Never break a user's turn because of a hook bug — fail open.
|
|
81
|
+
process.exit(0)
|
|
82
|
+
}
|
package/package.json
CHANGED