Skip to content

feat: add Linear guardrails to CLAUDE.md (SEC-20)#3

Merged
bryantb2 merged 3 commits intomainfrom
fix/linear-guardrails
Mar 29, 2026
Merged

feat: add Linear guardrails to CLAUDE.md (SEC-20)#3
bryantb2 merged 3 commits intomainfrom
fix/linear-guardrails

Conversation

@bryantb2
Copy link
Copy Markdown
Owner

Summary

  • Destructive Linear actions (delete tickets/projects, archive, remove members) require human approval
  • Bulk modifications capped at 5 tickets without asking
  • Read + status updates remain free

Closes SEC-20 from security audit.

bryantb2 and others added 3 commits March 28, 2026 18:45
…rails

- Install secret scanning pre-commit hook via git core.hooksPath in entrypoint.sh
- Hook blocks commits matching sk-ant-, ghp_, ghs_, lin_api_, PEM blocks, base64 JWT patterns
- Add Security Boundaries section to groups/CLAUDE.md with email policy (krewtrack.com free, external requires approval) and data protection rules
- Add DEFAULT_MAX_BUDGET_USD=5 to config.ts (overridable via env var)
- Wire maxBudgetUsd: DEFAULT_MAX_BUDGET_USD into Slack-triggered runs in index.ts
- Use DEFAULT_MAX_BUDGET_USD as fallback in task-scheduler.ts for scheduled tasks
- Add --cpus=2 to docker run args in container-runner.ts
Destructive Linear actions (delete tickets/projects, archive,
remove members) require human approval. Bulk modifications
capped at 5 tickets without asking. Read + status updates are free.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bryantb2 bryantb2 merged commit 20abd82 into main Mar 29, 2026
1 of 2 checks passed
bryantb2 added a commit that referenced this pull request Apr 7, 2026
- Remove direct dev-team→QA IPC path; let dispatch handle all QA
  routing via build loop to maintain hub-and-spoke model (issue #1)
- Add explicit CI re-poll instruction after pushing fixes (issue #2)
- Remove --depth=1 from repo clones to avoid branch op failures (issue #3)
- Parameterize repo name in QA screenshot protocol (issue #4)
- Use subshell for cd in setup script to avoid dir state issues (#7)
- Add default branch note to session startup (#5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bryantb2 added a commit that referenced this pull request Apr 7, 2026
…n, QA handoff, persistent repos (#15)

* fix: orchestration loop failures — completion records, CI verification, QA handoff

Root cause: dispatch→dev-team→qa-sentinel pipeline broken because
(1) completion record rules excluded dispatch-routed tasks,
(2) dev-team had no post-PR protocol for CI/QA/records,
(3) QA posted file paths instead of embedded screenshots.

Changes:
- Global CLAUDE.md: broaden completion record scope to include
  dispatch-routed tasks (identified by [DISPATCH-ROUTED] tag),
  add mandatory Engineer delegation prompt injections for commit
  discipline, CI verification, PR targeting, and completion records
- Dev-team CLAUDE.md: add Session Startup for persistent repos,
  Post-PR Protocol (CI polling, Linear comment, completion record,
  QA handoff via IPC), PR targeting rule
- QA-sentinel CLAUDE.md: add GitHub PR Screenshot Protocol
  (upload as release assets, embed URLs — never file paths),
  session startup for persistent repos
- Dispatch CLAUDE.md: add [DISPATCH-ROUTED] tag requirement for
  all IPC messages, scheduled task manifest requirement, update
  all IPC message templates with tag and explicit post-PR instructions
- Mount allowlist example: add ~/repos and ~/nanoclaw/data roots
- New script: setup-persistent-repos.sh for per-group repo clones

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review feedback on orchestration PR

- Remove direct dev-team→QA IPC path; let dispatch handle all QA
  routing via build loop to maintain hub-and-spoke model (issue #1)
- Add explicit CI re-poll instruction after pushing fixes (issue #2)
- Remove --depth=1 from repo clones to avoid branch op failures (issue #3)
- Parameterize repo name in QA screenshot protocol (issue #4)
- Use subshell for cd in setup script to avoid dir state issues (#7)
- Add default branch note to session startup (#5)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bryantb2 added a commit that referenced this pull request Apr 12, 2026
Code review of PR #40 (superpowers:code-reviewer subagent) flagged 4
Important issues. All addressed in this commit.

## Issue #1 — `rmSync` lacks `force: true` (TOCTOU vulnerability)

Previous helper used `if (existsSync(dest)) rmSync(dest)` — vulnerable
to a TOCTOU race if an operator/backup script deleted the dir between
the existsSync check and the rmSync call. The new implementation uses
`force: true` on every rmSync (both scratch cleanup and final swap),
making the helper resilient to concurrent external deletion.

## Issue #2 — Atomic rename pattern (highest-value hardening)

Previous helper did `rmSync(dest); cpSync(src, dest)` — if cpSync
failed mid-copy (disk full, EIO, permission revocation), the cache was
left partial and the next container's `tsc` step would crash on the
broken source tree. The reviewer correctly called this out as the
single highest-value hardening: low probability but high blast radius.

New implementation uses canonical atomic-rename:
1. `rmSync(scratchDir, { force: true })` — clean any leftover from
   prior failed run (no-op if absent thanks to force flag)
2. `cpSync(src, scratchDir)` — copy to scratch first; if this throws,
   the existing cache at dest is untouched
3. `rmSync(dest, { force: true })` — drop the old cache
4. `renameSync(scratchDir, dest)` — swap into place

If step 2 fails: existing cache intact, helper throws, next spawn
retries cleanly (and removes the partial scratch first).
If step 3 or 4 fails (extremely unlikely): wrapped in try/catch with
best-effort scratch cleanup so retries don't accumulate scratch dirs.

The window between step 3 and step 4 is sub-millisecond and protected
by GroupQueue.active serialization (only one spawn per group at a
time, verified by walking the call chain enqueueMessageCheck →
runForGroup → runContainerAgent → buildVolumeMounts).

## Issue #3 — Production logging on the refresh

Previous helper was silent in production. The reviewer correctly
noted: "the original bug went undetected for 2 days partly because
there was zero observability into the cache lifecycle." Added a
single `logger.debug` line on success and a `logger.error` line in
the catch block. Cheap, valuable forever.

## Issue #4 — Integration test pinning the wiring

The 11 unit tests in refresh-agent-runner-src-cache.test.ts cover
the helper's real wipe-and-recopy semantics, but none of them
verifies that buildVolumeMounts ACTUALLY CALLS the helper. A future
refactor that drops the helper invocation would not break a single
existing test and the populate-once regression would silently return.

Added a new "container-runner agent-runner cache refresh wiring"
describe block in container-runner.test.ts with 2 integration tests
that exercise the full runContainerAgent → buildVolumeMounts →
refreshAgentRunnerSrcCache code path and assert:

1. cpSync was called with the scratch dir target (verifies the
   atomic-rename pattern is wired through, not just direct cpSync
   into dest — if anyone reverts to in-place copy, this fails)
2. renameSync was called with `<dest>.new → dest` pair (verifies
   the swap step is wired)

Required adding `cpSync`, `rmSync`, `renameSync` to the global fs
mock in container-runner.test.ts (they were not previously used).

## Issue #6 (minor) — Stale "agents can customize it" comment

The buildVolumeMounts call site comment said "Copy agent-runner
source into a per-group writable location so agents can customize
it." That framing was already misleading before this PR (no group
actually customizes) and is now actively wrong (refresh wipes
customizations on every spawn). Updated the comment to point at
the helper's docstring and to explicitly warn against writing
runtime state into the cache dir.

## Tests added (6 net new)

In refresh-agent-runner-src-cache.test.ts (4 new):
- cleans up a leftover .new scratch dir from a prior failed refresh
- preserves the existing cache when cpSync fails mid-copy (disk
  full simulation via vi.spyOn, the scenario from Issue #2)
- cleans up scratch dir on failure so retries do not accumulate state
- does not throw when the dest directory does not exist (force flag,
  the scenario from Issue #1)

In container-runner.test.ts (2 new):
- invokes cpSync on the agent-runner src path during runContainerAgent
- invokes renameSync to swap the scratch dir into place

## Verification

- 587 / 587 tests passing locally (was 581, +6 new)
- npx tsc --noEmit: clean
- All 4 Important issues from code review addressed
- Issue #5 (filename naming nit) skipped — minor consistency note,
  not worth churn
- Issue #7 (symlink test) skipped — no symlinks in source today,
  acceptable to defer until needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
bryantb2 added a commit that referenced this pull request Apr 12, 2026
…ery spawn (#40)

* fix(container-runner): refresh per-group agent-runner src cache on every spawn

The per-group `data/sessions/<group>/agent-runner-src/` cache was
implemented in commit 5fb1064 (security fix qwibitai#392) as part of mounting
the project root read-only to prevent sandbox escape. Each group needs
its own writable copy of the agent-runner source so agents can
customize tools/behavior without affecting other groups.

But the cache was created with `if (!fs.existsSync(...)) cpSync(...)`
— populate-once-and-never-refresh. Once a group spawned its first
container, the cache was frozen forever. Every subsequent agent-runner
change was silently swallowed for that group, even after `restart-fleet.sh`
and submodule bumps.

## How the bug went undetected

NanoClaw's deploy flow gives a false sense of correctness:
1. `git pull` brings in new container/agent-runner/src/ — this works
2. `npm run build` produces new container/agent-runner/dist/ — this works
3. Container image build copies and recompiles — this works (image
   has the new code in /app/dist/index.js)
4. Container spawn bind-mounts /app/src ← per-group cache — STALE
5. Container's entrypoint.sh recompiles /app/src to /tmp/dist and
   runs from /tmp/dist — runs the STALE code

The image build at step 3 did the right thing, but its result is
shadowed by the stale cache mount at step 4. Investigators see fresh
code on the host, fresh code in /app/dist inside the container, and
no obvious failure — but the entrypoint compiles from /app/src
(stale) not /app/dist (fresh).

## Real-world impact

3 PRs merged into main between Apr 9 and Apr 11 never reached the
4 cached groups (slack_dispatch, slack_dev-team, slack_qa-sentinel,
slack_fleet-ops):

- aec983e (PR #29) feat(cost): accurate cost tracking from token counts
  Apr 9 22:26 MDT — host-side schema migration ran, but the agent-runner
  side never deployed. cost_log shows `cost_source='sdk'` with
  `input_tokens=0` for every dispatch row since Apr 9 because the
  cached version still uses the SDK-fallback path. We spent today
  investigating this as an "SDK quirk" — it was actually this bug.

- 6cfff71 (PR #38) chore: post-deploy cleanup nits
  Diagnostic logger for the (apparent) SDK token gap — never deployed.

- be22ff2 (PR #39) fix(agent-runner): write DIAG zero-token dump
  File-based diagnostic capture — never deployed.

The 2 groups WITHOUT a cache (slack_dev-ops, slack_product-brain)
will get fresh code on first spawn and don't share the bug. Once they
spawn their first container, they will inherit the same staleness
unless this fix lands.

## Fix

Replace the populate-once guard with unconditional refresh: wipe the
cache directory if it exists, then re-copy from the upstream source.
Per-group isolation is preserved because each group still gets its
own independent writable copy at a distinct path.

Extracted into `refreshAgentRunnerSrcCache(src, dest)` for testability,
called from `buildVolumeMounts` on every container spawn. ~10-50ms cp
overhead per spawn (recursive copy of ~33KB of TypeScript), well below
container spawn baseline.

## Tests added (7)

New file `src/refresh-agent-runner-src-cache.test.ts` (separate from
container-runner.test.ts which mocks `fs` globally):

1. creates the cache directory when it does not exist
2. REFRESHES a stale cache (the regression fix from PR #29 cache bug)
   — pre-seeds an Apr-9-style stale cache + a file that no longer
   exists in upstream, asserts both are replaced and cleaned
3. preserves per-group isolation (security fix qwibitai#392) — two groups get
   independent cache dirs; mutating one does not affect the other
4. refresh wipes per-group customizations (acceptable trade-off) —
   pins the design choice so a future feature relying on cross-spawn
   customization persistence will fail this test and force a redesign
5. is a no-op when the upstream source dir does not exist
6. idempotent: calling twice with no source changes produces identical
   result
7. handles nested subdirectories in the upstream source

581 / 581 tests passing locally (was 574, +7 new).

## Deploy impact warning

When this PR ships and `restart-fleet.sh` runs, the next container
spawn for ANY group will retroactively activate every agent-runner
change since Apr 9 for the 4 cached groups. That includes:
- PR #29's per-token cost computation → cost_log will start showing
  non-zero token counts for dispatch / dev-team / qa-sentinel /
  fleet-ops on the next spawn
- PR #38 + PR #39's diagnostic logging — both become live but neither
  is needed anymore since #29 is now actually doing what it should

Most behavior changes from #29 are pure observability improvements
(token counts in cost_log) so the deploy is low-risk. But Blake should
be aware that cost reporting will look DIFFERENT after deploy:
`cost_source` will switch from `'sdk'` to `'computed'` for cached
groups, and `input_tokens`/`output_tokens` will populate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style(test): prettier --write on refresh-agent-runner-src-cache.test.ts (CI format:check)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fixup(audit): atomic rename + force flag + logging + integration test

Code review of PR #40 (superpowers:code-reviewer subagent) flagged 4
Important issues. All addressed in this commit.

## Issue #1 — `rmSync` lacks `force: true` (TOCTOU vulnerability)

Previous helper used `if (existsSync(dest)) rmSync(dest)` — vulnerable
to a TOCTOU race if an operator/backup script deleted the dir between
the existsSync check and the rmSync call. The new implementation uses
`force: true` on every rmSync (both scratch cleanup and final swap),
making the helper resilient to concurrent external deletion.

## Issue #2 — Atomic rename pattern (highest-value hardening)

Previous helper did `rmSync(dest); cpSync(src, dest)` — if cpSync
failed mid-copy (disk full, EIO, permission revocation), the cache was
left partial and the next container's `tsc` step would crash on the
broken source tree. The reviewer correctly called this out as the
single highest-value hardening: low probability but high blast radius.

New implementation uses canonical atomic-rename:
1. `rmSync(scratchDir, { force: true })` — clean any leftover from
   prior failed run (no-op if absent thanks to force flag)
2. `cpSync(src, scratchDir)` — copy to scratch first; if this throws,
   the existing cache at dest is untouched
3. `rmSync(dest, { force: true })` — drop the old cache
4. `renameSync(scratchDir, dest)` — swap into place

If step 2 fails: existing cache intact, helper throws, next spawn
retries cleanly (and removes the partial scratch first).
If step 3 or 4 fails (extremely unlikely): wrapped in try/catch with
best-effort scratch cleanup so retries don't accumulate scratch dirs.

The window between step 3 and step 4 is sub-millisecond and protected
by GroupQueue.active serialization (only one spawn per group at a
time, verified by walking the call chain enqueueMessageCheck →
runForGroup → runContainerAgent → buildVolumeMounts).

## Issue #3 — Production logging on the refresh

Previous helper was silent in production. The reviewer correctly
noted: "the original bug went undetected for 2 days partly because
there was zero observability into the cache lifecycle." Added a
single `logger.debug` line on success and a `logger.error` line in
the catch block. Cheap, valuable forever.

## Issue #4 — Integration test pinning the wiring

The 11 unit tests in refresh-agent-runner-src-cache.test.ts cover
the helper's real wipe-and-recopy semantics, but none of them
verifies that buildVolumeMounts ACTUALLY CALLS the helper. A future
refactor that drops the helper invocation would not break a single
existing test and the populate-once regression would silently return.

Added a new "container-runner agent-runner cache refresh wiring"
describe block in container-runner.test.ts with 2 integration tests
that exercise the full runContainerAgent → buildVolumeMounts →
refreshAgentRunnerSrcCache code path and assert:

1. cpSync was called with the scratch dir target (verifies the
   atomic-rename pattern is wired through, not just direct cpSync
   into dest — if anyone reverts to in-place copy, this fails)
2. renameSync was called with `<dest>.new → dest` pair (verifies
   the swap step is wired)

Required adding `cpSync`, `rmSync`, `renameSync` to the global fs
mock in container-runner.test.ts (they were not previously used).

## Issue #6 (minor) — Stale "agents can customize it" comment

The buildVolumeMounts call site comment said "Copy agent-runner
source into a per-group writable location so agents can customize
it." That framing was already misleading before this PR (no group
actually customizes) and is now actively wrong (refresh wipes
customizations on every spawn). Updated the comment to point at
the helper's docstring and to explicitly warn against writing
runtime state into the cache dir.

## Tests added (6 net new)

In refresh-agent-runner-src-cache.test.ts (4 new):
- cleans up a leftover .new scratch dir from a prior failed refresh
- preserves the existing cache when cpSync fails mid-copy (disk
  full simulation via vi.spyOn, the scenario from Issue #2)
- cleans up scratch dir on failure so retries do not accumulate state
- does not throw when the dest directory does not exist (force flag,
  the scenario from Issue #1)

In container-runner.test.ts (2 new):
- invokes cpSync on the agent-runner src path during runContainerAgent
- invokes renameSync to swap the scratch dir into place

## Verification

- 587 / 587 tests passing locally (was 581, +6 new)
- npx tsc --noEmit: clean
- All 4 Important issues from code review addressed
- Issue #5 (filename naming nit) skipped — minor consistency note,
  not worth churn
- Issue #7 (symlink test) skipped — no symlinks in source today,
  acceptable to defer until needed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: prettier --write on container-runner.ts + cache helper test (CI format:check)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant