@drawbridge/drawbridge-agents 0.1.97 → 0.1.99

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.
@@ -21,12 +21,9 @@
21
21
  "../drawbridge-components",
22
22
  "../drawbridge-docs",
23
23
  "../drawbridge-emails",
24
- "../drawbridge-games",
25
- "../drawbridge-growth",
26
24
  "../drawbridge-mongodb",
27
25
  "../drawbridge-redis",
28
26
  "../drawbridge-share",
29
- "../drawbridge-shopify",
30
27
  "../drawbridge-shopify-app",
31
28
  "../drawbridge-stripe",
32
29
  "../drawbridge-sync",
@@ -56,6 +53,15 @@
56
53
  },
57
54
  "hooks": {
58
55
  "PreToolUse": [
56
+ {
57
+ "matcher": "Read|Bash",
58
+ "hooks": [
59
+ {
60
+ "type": "command",
61
+ "command": "node ./.claude/hooks/guard-secrets.js"
62
+ }
63
+ ]
64
+ },
59
65
  {
60
66
  "matcher": "Write|Edit|MultiEdit|NotebookEdit",
61
67
  "hooks": [
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
@@ -3,6 +3,7 @@
3
3
  @../conventions/karpathy-guidelines.md
4
4
  @../conventions/rules.md
5
5
  @../conventions/verification.md
6
+ @../conventions/secrets.md
6
7
  @../conventions/javascript-formatting.md
7
8
  @../conventions/nested-objects.md
8
9
  @../conventions/jsx-fragments.md
@@ -24,3 +25,4 @@
24
25
  @../conventions/walk-the-app.md
25
26
  @../conventions/feature-pipeline.md
26
27
  @../conventions/workflow-step-naming.md
28
+ @../conventions/token-budget.md
@@ -107,15 +107,17 @@ skill) so it stops living only in tribal memory.
107
107
  carry `organization : null` and still block everyone;
108
108
  api registers the number's SmsUrl at twilio connect (`lib/twilio.js`) and `scripts.js
109
109
  registerTwilioWebhooks` backfills — a deployed webhooks route with unregistered numbers hears
110
- nothing, silently. **Since Butterscotch (release 1219790090574059; sync 762fdd7, Darren
111
- 2026-10-23: an opt-out is the person's choice, not one address's) `canSend` gates the
112
- PERSON, not only the address**: it reads the recipient's contact in the organization (found by
113
- `canonical.emails`/`canonical.phones` or by `leads`) and blocks when any canonical or raw
114
- address on that contact is suppressed for the channel. `lead` is optional, so every caller
115
- that knows the lead passes it, or a returning person under a new mailbox passes until their
116
- contact is rebuilt with the new address. The api's draw opt-out check
117
- (`route/organization-campaign-draw.js` `optedOutLeads`) applies the same rule and must stay
118
- in step with it; before Butterscotch both matched the lead's own canonical address only.
110
+ nothing, silently. **Every message send and every draw checks the recipient's OWN canonical
111
+ address, never their contact** (Darren, 2026-10-29): contacts exist only once an organization
112
+ activates segments, and sync merges entrants who share a phone into one contact, so a
113
+ contact-reading gate blocked a second entrant for the first one's unsubscribe, and only where
114
+ segments was on. The api's draw check (`lib/opt-out.js`, used by prepare, the draw and the
115
+ winner send) and `canSend` without `person` must stay in step. **Since Butterscotch
116
+ (release 1219790090574059) the ONE caller that reads the contact is the vendor contact sync**:
117
+ `lib/step-runner.js` passes `person : lead.id` for the `suppressed` answer the
118
+ Mailchimp/Klaviyo/Attentive hooks receive, and `canSend` then blocks when any canonical or raw
119
+ address on the person's contact (found by `leads` or `canonical.emails`/`canonical.phones`) is
120
+ suppressed. A message path that passes `person` reintroduces the shared-phone bug.
119
121
  - **`audience` picks the FROM-address; `intent` picks the unsubscribe plumbing — and NOTHING
120
122
  passes the suppression gate** (utils ↔ sync ↔ api). Every suppression source blocks every
121
123
  lead-facing send, the winner claim included: api's draw route refuses up front ("Disqualify
@@ -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,37 @@
1
+ # Token budget
2
+
3
+ Darren works on a subscription, and the rolling limits are metered in tokens, so a QA run or
4
+ a full dev walk that spends fewer tokens is one that finishes before the limit lands
5
+ (Darren, 2026-11-02: "we don't want to stop doing those things, we just want a way to use less
6
+ tokens when we do"). Two rules, both about where the tokens actually go: tool output and
7
+ subagents, not the prompt.
8
+
9
+ ## Read-only subagents run on Haiku
10
+
11
+ A subagent that only locates things — `Explore`, `Plan`, a `general-purpose` agent sent to
12
+ search or read — is spawned with `model : 'haiku'`. It returns file paths and conclusions,
13
+ and the session model reads the files it names. Agents that judge or write keep the session
14
+ model: `drawbridge-qa` (walks and reports), the code reviewer, anything that edits. A Haiku
15
+ agent's "not found" is never evidence of absence under rule 6 in `rules.md`: before claiming
16
+ nothing writes, reads or documents a thing, the session model runs that search itself.
17
+
18
+ ## Long output is summarised by the command, not by the model
19
+
20
+ A build, test, log or list command that can print more than a screen is piped through `tail`
21
+ for its summary AND `grep -i -E 'fail|error'` for its failures, in the same command. A bare
22
+ `tail` hides the failing line; a bare dump spends the tokens. Before a long walk, `/compact`;
23
+ between unrelated tasks, a new session rather than a longer one.
24
+
25
+ ## Locate with Serena, read with Read
26
+
27
+ Serena (an MCP server, `oraios/serena`, registered at user scope) finds a symbol and its
28
+ references through a language server. Use it to find WHERE code lives instead of reading
29
+ whole files or paging through grep hits. It does not replace the read: rule 5 in `rules.md`
30
+ still means the whole file is open before a claim about it, and rule 6 still means absence is
31
+ proved by a search of the whole space. Serena only shortens the search for the file to open.
32
+
33
+ Trial since 2026-11-02 on Darren's machine (install: `uv tool install -p 3.13 serena-agent`,
34
+ then `claude mcp add --scope user serena -- serena start-mcp-server --context claude-code
35
+ --project-from-cwd`; `**/.serena/` is in the global git ignore). The agents sync preflight
36
+ installs it family-wide only once the trial shows file reads dropping. Serena's own README
37
+ says never to install it from a plugin or MCP marketplace.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drawbridge/drawbridge-agents",
3
- "version": "0.1.97",
3
+ "version": "0.1.99",
4
4
  "description": "Shared agent-instruction content (rules, code style, conventions) for the drawbridge-* monorepo.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {