claude-mcp-workflow 0.1.7 → 0.1.9
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.
- package/.claude-plugin/plugin.json +4 -2
- package/README.md +39 -2
- package/build/engine.d.ts +21 -1
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +59 -6
- package/build/engine.js.map +1 -1
- package/build/storage.d.ts +20 -0
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +41 -0
- package/build/storage.js.map +1 -1
- package/build/types.d.ts +21 -20
- package/build/types.d.ts.map +1 -1
- package/build/types.js +2 -1
- package/build/types.js.map +1 -1
- package/hooks/hooks.json +4 -2
- package/hooks/workflow-cleanup.sh +58 -20
- package/hooks/workflow-start.sh +60 -32
- package/package.json +1 -1
- package/templates/bug-fix.yaml +98 -16
- package/templates/code-review.yaml +30 -12
- package/templates/coding.yaml +49 -4
- package/templates/debugging.yaml +39 -14
- package/templates/explore.yaml +48 -14
- package/templates/file-code.yaml +13 -4
- package/templates/file-review.yaml +25 -6
- package/templates/github-init.yaml +24 -8
- package/templates/investigate.yaml +13 -4
- package/templates/master.yaml +16 -13
- package/templates/new-feature.yaml +24 -26
- package/templates/planning.yaml +38 -7
- package/templates/reflection.yaml +11 -4
- package/templates/review-push.yaml +26 -7
- package/templates/skills/architecture/SKILL.md +131 -1
- package/templates/skills/aws-lambda/SKILL.md +9 -9
- package/templates/skills/browser-verify/SKILL.md +58 -0
- package/templates/skills/build-cmake/SKILL.md +24 -8
- package/templates/skills/ci-github-actions/SKILL.md +34 -18
- package/templates/skills/claude-code-config/SKILL.md +41 -19
- package/templates/skills/cleanup/SKILL.md +57 -0
- package/templates/skills/coding-skill-selector/SKILL.md +2 -1
- package/templates/skills/debug-bridge-scaffold/SKILL.md +98 -0
- package/templates/skills/domain-gamedev/SKILL.md +56 -6
- package/templates/skills/domain-pixi/SKILL.md +256 -7
- package/templates/skills/domain-reid/SKILL.md +39 -5
- package/templates/skills/domain-yolo/SKILL.md +18 -7
- package/templates/skills/ide-zed/SKILL.md +23 -0
- package/templates/skills/lang-as3/SKILL.md +7 -5
- package/templates/skills/lang-haxe/SKILL.md +538 -19
- package/templates/skills/lang-python/SKILL.md +6 -2
- package/templates/skills/math/SKILL.md +64 -2
- package/templates/skills/mcp-setup/SKILL.md +14 -2
- package/templates/skills/preferences/SKILL.md +10 -10
- package/templates/skills/skill-manager/SKILL.md +264 -0
- package/templates/skills/target-openfl-native/SKILL.md +52 -17
- package/templates/skills/target-openfl-native/references/build-and-versions.md +7 -4
- package/templates/skills/task-delegation/SKILL.md +76 -4
- package/templates/skills/web-reading/SKILL.md +33 -25
- package/templates/skills/workflow-authoring/SKILL.md +47 -12
- package/templates/subagent.yaml +29 -8
- package/templates/testing.yaml +64 -10
- package/templates/web-research.yaml +23 -13
|
@@ -21,7 +21,7 @@ Three questions (if you can't answer all three concretely, suggest the simpler o
|
|
|
21
21
|
**GOTCHA:** Single implementation behind interface is ceremony, not architecture
|
|
22
22
|
**EXCEPTION:** Public library APIs only
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Models often default to "interfaces for testing are fine" — this skill corrects that.
|
|
25
25
|
|
|
26
26
|
## Response Template
|
|
27
27
|
|
|
@@ -107,6 +107,17 @@ When moving filtering logic from a caller into a shared state manager (allocator
|
|
|
107
107
|
|
|
108
108
|
**Rule:** A generic `isX()` that returns the same answer for semantically different states is a lossy abstraction. Before using it, check: does the original code care *why* the state is set, not just *that* it's set?
|
|
109
109
|
|
|
110
|
+
## Deleting a Mechanism for ONE Rejected Job — Enumerate ALL Its Jobs First
|
|
111
|
+
|
|
112
|
+
When you remove a function/call because one of its responsibilities is rejected or obsolete, list **every** side effect it has before deleting it. A mechanism named for one concern often quietly carries a second, still-needed concern — and the name masks it, so wholesale deletion silently disables the second.
|
|
113
|
+
|
|
114
|
+
**DON'T:** See "this does the rejected X" → delete the whole call/function.
|
|
115
|
+
**DO:** Trace every effect (what it sets, reveals, schedules, gates). Keep the still-needed effects; remove only the rejected one — often by splitting the dual-purpose function.
|
|
116
|
+
|
|
117
|
+
**Tell:** the deleted thing was a per-frame/lifecycle hook whose name describes only one job (`dodge`, `sync`, `refresh`); the regression appears in an *adjacent* concern (visibility, enablement, layout), not the one you were targeting — and surfaces late (doc/visual pass), not in the correctness check.
|
|
118
|
+
|
|
119
|
+
**Concrete example (illustrative):** A per-frame `noteActorPosition` did TWO things via one alpha lane — faded decorative props IN (made the field *visible*) and faded them OUT near the moving actor (the *proximity dodge*). The user rejected the dodge (alpha-hiding the path). Deleting the whole call removed the dodge **and** the field's visibility → the entire decorative prop field went invisible (`alpha=0` forever). Fix: split — keep a one-time fade-IN (reveal) gated by a `fadeIn` flag, drop only the per-frame fade-OUT. The reveal and the dodge had been fused in one function; the name ("dodge") hid the reveal.
|
|
120
|
+
|
|
110
121
|
## Refactoring Scope: Don't Escalate Lifecycle
|
|
111
122
|
|
|
112
123
|
When refactoring an inline operation into a two-phase approach (set intent + execute), match the original operation's lifecycle scope.
|
|
@@ -118,6 +129,23 @@ When refactoring an inline operation into a two-phase approach (set intent + exe
|
|
|
118
129
|
|
|
119
130
|
**Rule:** Before adding lifecycle management to a refactored operation, check: did the original operation need it? If the original worked within existing infrastructure (setters, listeners), the refactored version should too.
|
|
120
131
|
|
|
132
|
+
## Signature Change → Grep All Callsites
|
|
133
|
+
|
|
134
|
+
When changing a method's return type, parameter list, or visibility, grep ALL callsites BEFORE finalizing the change.
|
|
135
|
+
|
|
136
|
+
A signature change compiles fine in the modified file but silently breaks callers that ignore the new return type. Example: `clearData(): void` → `clearData(): ITask` — callers that fired-and-forgot still compile; the returned ITask is constructed and discarded; the async work never starts. No compile error, no exception — the cleanup pipeline is a no-op.
|
|
137
|
+
|
|
138
|
+
**Symptom**: behavior silently regresses in code paths nobody runs during initial smoke test. Caught later by code reviewers, integration tests, or production bugs.
|
|
139
|
+
|
|
140
|
+
**DON'T:** Change a signature, run the obvious test, declare done.
|
|
141
|
+
**DO:** Change a signature, grep `<methodName>\b` across the whole project, audit each callsite. Trust "find references" only if the IDE has full project indexing.
|
|
142
|
+
|
|
143
|
+
**Adding a member to an interface/abstract type is also a signature change.** Every implementor must gain the member — including hand-written test doubles / fakes / in-memory stubs, not just production classes.
|
|
144
|
+
|
|
145
|
+
**Tooling gotcha:** test runners that strip types via esbuild/swc (vitest, ts-jest `isolatedModules`, bun test) do NOT typecheck. A test double missing a newly-added interface method compiles fine and fails at RUNTIME as `TypeError: x.method is not a function` — often in unrelated tests that happen to reach the missing call. `tsc --noEmit` catches it; the test run alone does not.
|
|
146
|
+
|
|
147
|
+
**DO:** after adding an interface member, grep for `implements <Interface>` and for the type used as a field/param type, and update every implementor (prod + test) in the same change; run `tsc` (not just the test suite) before declaring done.
|
|
148
|
+
|
|
121
149
|
## New Canonical Path → Ask About Legacy Parallels
|
|
122
150
|
|
|
123
151
|
When you add a new canonical input/output mechanism that does the same thing as existing legacy paths (e.g. UI buttons that replicate keyboard shortcuts, a typed API replacing an untyped one, a structured event replacing a string topic), the default urge is to keep the legacy paths "for compatibility" and build a coexistence shim. Don't.
|
|
@@ -130,3 +158,105 @@ When you add a new canonical input/output mechanism that does the same thing as
|
|
|
130
158
|
**Symptom of violation:** you find yourself adding gating logic (coordinate checks, `if (ev.target === ...)` filters, `inflight` flags) just to make two input paths coexist when one is meant to subsume the other.
|
|
131
159
|
|
|
132
160
|
**Rule:** A new canonical mechanism is an opportunity to remove the old one, not to add a referee between them. If the user didn't explicitly ask to keep both, ask before keeping both.
|
|
161
|
+
|
|
162
|
+
## Accumulation/Leak Bugs: Fix the Lifecycle, Not a Sweep
|
|
163
|
+
|
|
164
|
+
When something grows unbounded (state files, DB rows, temp dirs, handles, cache entries), the artifact has a creation event but no deletion event tied to its owner's lifecycle.
|
|
165
|
+
|
|
166
|
+
**DON'T:** Add a periodic retention/TTL/prune sweep ("delete things older than N days"). It leaves the leak in place, adds a tunable nobody sets correctly, and needs a guard against its own edge cases (NaN window deleting everything).
|
|
167
|
+
**DO:** Tie the artifact's lifetime to its owner. Delete it at the terminal transition (session ends → its file is removed; process dies → its records go). The owner that creates it owns destroying it.
|
|
168
|
+
|
|
169
|
+
**Tell:** if your fix introduces a `RETENTION_DAYS`/`maxAge`/cleanup-cron, ask "why does this outlive its owner at all?" A retention sweep is a legitimate *policy* for audit data deliberately kept; it is the wrong tool for a missing teardown.
|
|
170
|
+
|
|
171
|
+
**Before designing the fix:** check sibling/related projects for an already-shipped fix of the same bug class (same author/org repos especially) and mirror its approach instead of inventing a parallel one.
|
|
172
|
+
|
|
173
|
+
## Repeated Same Symptom After 2+ Fixes = Wrong Subsystem Target
|
|
174
|
+
|
|
175
|
+
When the user reports the same perceived symptom after two or more rounds of plausible fixes, stop patching that surface. A symptom that survives multiple reasonable fixes is almost always at the **seam between two decoupled subsystems** — a baked/precomputed path vs. a live event; a cached value vs. its source; a predicted position vs. an independently-simulated actor.
|
|
176
|
+
|
|
177
|
+
**DON'T:** Keep refining one side each iteration — curve shape, easing, constants, thresholds — to match the user's latest wording.
|
|
178
|
+
|
|
179
|
+
**DO:** By the 2nd repeat, trace BOTH subsystems end-to-end and ask: "why do these two representations exist, and must they?" The fix is to make them **coincide** — re-derive one from the other at the authoritative moment — not to tune one side.
|
|
180
|
+
|
|
181
|
+
**Tell:** Every "fix" addresses the user's latest description verbatim, yet the user keeps saying "same thing / didn't help." You find yourself adjusting geometry or timing constants repeatedly with no lasting effect.
|
|
182
|
+
|
|
183
|
+
**GOTCHA:** The seam is invisible when you look at only one subsystem. You must trace both from their shared input to their diverging output paths to see the gap.
|
|
184
|
+
|
|
185
|
+
**Concrete example:** A game ball flew a fixed pre-baked bézier to a predicted endpoint; the actual hit fired from a per-frame overlap test against a separately-simulated moving sprite. Four curve-shape fixes (overshoot, speed, bulge, straight-line) all failed because the real defect was the timing seam between the two decoupled systems. Resolution: re-derive the sprite's velocity at the true flight start so ball and sprite converge at one shared point. Cost: 5 user round-trips before the architecture was traced.
|
|
186
|
+
|
|
187
|
+
## Regressing Feature? Find the Principled Algorithm the Codebase Already Has
|
|
188
|
+
|
|
189
|
+
When a feature keeps regressing across many patch cycles — tuning constants, reshaping curves, adjusting thresholds — stop inventing ad-hoc logic. Ask: **"what does correct behavior fundamentally require, and does this codebase already compute that for some other case?"**
|
|
190
|
+
|
|
191
|
+
**DON'T:** Keep hand-rolling case-specific geometry or heuristics to chase the latest symptom. Each variant is "more specific" than the last and never converges.
|
|
192
|
+
|
|
193
|
+
**DO:** Grep for the concept (clearance, retry, normalization, lift…). If a general implementation exists, find why the failing case is excluded from it — an early `return`, a zone guard, a capability flag. That exclusion is the smoking gun: someone already solved it generally, then deliberately opted out the hard case. Mirror or apply that proven algorithm to the excluded case, using its same constants and sampling strategy for parity. Ask the "do we already do it elsewhere?" question by the **2nd** regression, not the 6th.
|
|
194
|
+
|
|
195
|
+
**Tell:** You're writing the Nth variant of the same code path and each fix is narrower than the last. A sibling module has a richer version of the same routine that the failing path doesn't call.
|
|
196
|
+
|
|
197
|
+
**GOTCHA:** The exclusion guard often looks load-bearing (e.g., "air shots have no terrain"). Verify it with the principled algorithm before assuming it's correct — it may just be an early simplification that was never revisited.
|
|
198
|
+
|
|
199
|
+
**Concrete example (illustrative):** An air-launched projectile kept clipping terrain through ~6 patch iterations (overshoot tweaks, speed adjustments, straight-line approximations). The math package already had a `raiseArcOverTerrain` routine — raises a bézier peak until it clears terrain minus a margin — but it explicitly `return`ed early for `zone === 'air'`. Fix: mirror that exact algorithm renderer-side for the excluded zone. No amount of curve reshaping could have matched it because the principled clearance math was simply never reached.
|
|
200
|
+
|
|
201
|
+
## Changing a Value Invalidates State Sized From Its OLD Value — Not Just Live Readers
|
|
202
|
+
|
|
203
|
+
When you change a parameter X, the instinct is to update code that **reads** X. But state that was **computed/staged earlier** from X's old value (a precomputed position, a cached offset, a buffer size, a timeout deadline) doesn't "read" anything — it silently encodes the stale quantity. Those frozen values contradict the new X, often by the full ratio of old/new.
|
|
204
|
+
|
|
205
|
+
**DON'T:** Change X, fix the obvious live consumers, ship. The stale-derived state then fires with pre-change geometry/timing intact.
|
|
206
|
+
|
|
207
|
+
**DO:** When changing X, enumerate not just "who reads X now" but "what was sized/positioned/scheduled FROM X (possibly long before X changed)." Re-derive those from the new X, or trigger their recompute at the moment X takes effect.
|
|
208
|
+
|
|
209
|
+
**Tell:** a downstream actor moves/scales ~the ratio of old:new too far/fast (e.g. ~10×); a thing that used to line up now overshoots by a suspiciously clean multiple; bug magnitude ≈ `oldValue / newValue`.
|
|
210
|
+
|
|
211
|
+
**GOTCHA:** The stale computation may happen at a different time (launch, init, precompute pass) than the code you just changed (tick, render, callback). Grep for every site that captures a derived value from X into a local variable, struct field, or closure — those are the freeze points.
|
|
212
|
+
|
|
213
|
+
**Concrete example:** A projectile's flight duration was shortened ~6×. The flight-playback code was updated. But an interceptor's ENTRY POSITION had been staged at launch as `start + speed · OLD_duration` (far away, sized for the long flight); the resync only re-solved its velocity over that stale far position → it streaked in ~10× too fast. Fix: re-STAGE the entry from the new duration (`position + velocity · newDuration`), not just re-solve velocity. The corrected duration must drive every derived value — including positions frozen before the change — from a single source.
|
|
214
|
+
|
|
215
|
+
## "Impossible in the Limit" ≠ "Cheap Lever Fails the Actual Case"
|
|
216
|
+
|
|
217
|
+
When a global constant's effect scales with a per-case input, no single value can satisfy ALL inputs — that proof is correct. But it does NOT establish that the constant fails the specific case in front of you.
|
|
218
|
+
|
|
219
|
+
**DON'T:** Write an elegant impossibility argument for the general case and propose a per-case solver/refactor without having run the simple fix once.
|
|
220
|
+
**DO:** Try the cheapest knob empirically first — adjust the constant, widen the relevant tolerance, measure the real target. Reserve the heavy solution for when the simple lever is *measured* to fail, not when it's only *proven* imperfect in the limit.
|
|
221
|
+
|
|
222
|
+
**Principle:** "Can't be perfect for all" ≠ "doesn't work for this." Empirical failure on the actual target is required evidence; theoretical failure on adversarial inputs is not.
|
|
223
|
+
|
|
224
|
+
**Tell:** You're composing a sound argument for why a simple fix "structurally cannot work" and proposing architecture instead — without having run the simple fix once. That's the signal to stop, try the knob, and measure.
|
|
225
|
+
|
|
226
|
+
## A Claimed-Negative Tradeoff Needs the Same Evidence as a Claimed-Impossible Fix
|
|
227
|
+
|
|
228
|
+
The mirror of "Impossible in the Limit ≠ Cheap Lever Fails": just as you must not call a simple fix impossible without running it, do not tell the user a change makes things WORSE without deriving the actual result. A negative framing extrapolated from a stale note, a prior decision, or an intuitive "it'll scatter / fragment / break" mental model is not evidence.
|
|
229
|
+
|
|
230
|
+
**DON'T:** Warn "this will be markedly worse" / "destroys the existing structure" from memory or intuition, then ask the user whether to proceed.
|
|
231
|
+
**DO:** Run the concrete analysis that confirms or refutes the claim FIRST — then present the measured result, and only flag a downside that survives the measurement.
|
|
232
|
+
|
|
233
|
+
**Tell:** You're about to present a tradeoff as a downside, and your reason is a remembered decision ("we left this alone before") or a shape you pictured ("members will scatter"), not something you computed on the actual artifact.
|
|
234
|
+
|
|
235
|
+
**Concrete example:** Asked to apply an automated member-reorder to a 12k-line god-file, the agent told the user it would be "markedly worse" — destroying intentional locality, exploding one guard block into scattered fragments — citing a stale "leave this file alone" note. The user pushed back: "why worse? I expect better." The analysis the agent should have done first: a sorted line-multiset diff (proved the reorder was a pure permutation — zero content changed) and an index-contiguity + identical-condition-dedup check (proved the guarded cluster stayed one contiguous block). The real change was mild and arguably cleaner. The unmeasured negative cost a user round-trip.
|
|
236
|
+
|
|
237
|
+
**Principle:** "I think this is worse" is a hypothesis, not a finding. Before it reaches the user as a reason to hesitate, derive it on the real input — the cost of measuring is usually one command; the cost of a wrong negative is a wasted round-trip and a user who now distrusts your tradeoff calls.
|
|
238
|
+
|
|
239
|
+
## "How Did It Work Before?" = Revert Signal
|
|
240
|
+
|
|
241
|
+
When the user asks "how did the old way work?", "why not keep it simple / slightly different?", or "did you even try the original approach?" — that is not a request to polish the new mechanism. It is evidence the new direction is wrong.
|
|
242
|
+
|
|
243
|
+
**DON'T:** Defend the new design across multiple fix→verify cycles, trading one symptom for another, while the user keeps invoking prior behavior.
|
|
244
|
+
**DO:** By the 2nd such pushback, diff against the pre-change baseline. Articulate honestly what the original did and why it was adequate. Offer to revert before sinking more cycles.
|
|
245
|
+
|
|
246
|
+
**Tell:** You are on the 3rd+ iteration fixing your own new mechanism; each fix introduces a new symptom; the user's questions consistently reference what it used to do. The thrash itself is the signal — the baseline was right.
|
|
247
|
+
|
|
248
|
+
**Principle:** Repeated pushback that references prior/simpler behavior is not polish feedback — it is a direction correction. Reverting to a known-good baseline is a first-class fix, not a failure.
|
|
249
|
+
|
|
250
|
+
## The User's Named Beat IS the Acceptance Criterion
|
|
251
|
+
|
|
252
|
+
When the requirement names a concrete observable event — a strike, a bounce, a wobble, a specific sequence — that beat is not a flourish. It is the spec. Reproducing the surrounding motion while omitting the named beat is a wrong implementation, not a simpler one.
|
|
253
|
+
|
|
254
|
+
**Observed failure:** User asks for "X hits a corner and visibly bounces, then branches." Agent repeatedly delivers a fast ease-to-stop or a smooth pass-through — dropping the bounce — and declares it done. Each simplification felt cleaner or more physically plausible. User rejected it each time: "there is no bounce, this is identical to the plain case." Required explicit re-statement of the task before the beat was built.
|
|
255
|
+
|
|
256
|
+
**Tell:** You're choosing a motion because it's easier to implement or "reads cleaner," and the specific event the user named is no longer distinctly visible in the result. Or: the user says the new thing "looks the same as the old/plain one."
|
|
257
|
+
|
|
258
|
+
**DON'T:** Trade away a named beat for implementation simplicity or "physical plausibility" concerns without surfacing the trade-off.
|
|
259
|
+
**DO:**
|
|
260
|
+
1. Enumerate every named beat from the request.
|
|
261
|
+
2. For each, define how it will be **distinctly observable** (numerically or visually) and assert it before claiming done.
|
|
262
|
+
3. If you're about to drop a beat because it's hard or feels unphysical, surface that trade-off explicitly — don't silently omit it.
|
|
@@ -7,30 +7,30 @@ description: AWS Lambda .NET deployment gotchas
|
|
|
7
7
|
|
|
8
8
|
## VPC Networking
|
|
9
9
|
|
|
10
|
-
- **
|
|
11
|
-
- **ENI
|
|
10
|
+
- **ENIs are shared since the Hyperplane rework (2019)** — one ENI per subnet + security-group combo, created when the function is created or its VPC config changes, NOT per concurrent execution. Scale-up does not consume a subnet IP per instance, so subnet IP exhaustion from Lambda scaling is largely a pre-2019 concern.
|
|
11
|
+
- **ENI provisioning takes up to ~90s** at function create / VPC-config change; the function reports `LastUpdateStatus: InProgress` until done. (The old per-invoke ENI cold-start penalty is gone.)
|
|
12
12
|
- **S3/Secrets Manager calls timeout silently (not connection refused)** when Lambda is in VPC without NAT/VPC Endpoint. Easy to misdiagnose as SDK bug.
|
|
13
13
|
|
|
14
14
|
## Logging
|
|
15
15
|
|
|
16
|
-
- **`Console.Error.WriteLine()`
|
|
16
|
+
- **`Console.Error.WriteLine()` IS captured by CloudWatch on the managed .NET runtime** (Lambda redirects the process's stdout/stderr) — but entries lack the request-id prefix and multi-line output splits into separate events. Output genuinely disappears only in narrower cases: writes during a crashed INIT (process torn down before flush) or a custom-runtime bootstrap that doesn't forward stderr. Prefer `ILambdaContext.Logger.LogLine()` in handler code or `ILogger` in the ASP.NET pipeline.
|
|
17
17
|
- **Missing `logs:CreateLogGroup` permission = logs silently lost.** Log group created lazily on first invoke. No permission = no log group = nowhere to write = no error visible anywhere.
|
|
18
18
|
|
|
19
19
|
## ASP.NET Core in Lambda
|
|
20
20
|
|
|
21
|
-
- **`UseHttpsRedirection()` hangs Lambda.** No Kestrel HTTPS port. Middleware hangs trying to discover one (aws-lambda-dotnet #1543). Only enable in Development.
|
|
22
|
-
- **Response > 6MB
|
|
21
|
+
- **`UseHttpsRedirection()` hangs Lambda.** No Kestrel HTTPS port. Middleware hangs trying to discover one (aws-lambda-dotnet #1543; discussion #1424 reports the same shape — handler completes but the response never reaches the runtime — as warm-invoke middleware timeouts on a custom runtime). Only enable in Development.
|
|
22
|
+
- **Response > 6MB fails the invoke — no clean HTTP 413.** 6MB is the synchronous INVOKE-phase request/response payload limit (nothing to do with INIT). `APIGatewayHttpApiV2ProxyFunction` surfaces it as a runtime-client error for that invocation, so the caller sees a generic 500-style failure instead of a 413.
|
|
23
23
|
|
|
24
24
|
## S3 from Lambda
|
|
25
25
|
|
|
26
26
|
- **SDK default timeout = infinite, retry = 4 with exponential backoff.** Total wait 30-60s can exceed Lambda timeout. Always set `Timeout` and `MaxErrorRetry` on `AmazonS3Config`.
|
|
27
|
-
-
|
|
28
|
-
- **IMDS
|
|
27
|
+
- **Region fallback to `us-east-1` is a local-dev gotcha, not a Lambda one.** Inside Lambda the SDK resolves the region from the `AWS_REGION` env var the service injects. `new AmazonS3Client()` without explicit region silently targets `us-east-1` only when no env/profile region is configured — e.g. local integration tests — causing slow cross-region requests with no error.
|
|
28
|
+
- **IMDS does not exist inside Lambda.** Credentials come from env vars (`AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`) injected from the execution role. Any "IMDS credential error is cached until restart" advice applies to EC2/ECS, not Lambda.
|
|
29
29
|
|
|
30
30
|
## .NET Cold Start
|
|
31
31
|
|
|
32
|
-
- **ReadyToRun (R2R)
|
|
33
|
-
- **
|
|
32
|
+
- **ReadyToRun (R2R) output is RID-specific — publish with `--runtime linux-x64` (or `linux-arm64`).** Since .NET 6, crossgen2 supports cross-OS compilation, so building ON macOS/Windows is fine as long as the target RID matches the Lambda runtime (dotnet8 runs on Amazon Linux 2023). R2R for the wrong RID silently has no effect.
|
|
33
|
+
- **INIT-phase billing was standardized 2025-08: INIT is billed for ALL runtimes and packaging types.** Previously ZIP-deployed managed runtimes got the INIT phase free (container images were always billed). Heavy .NET startup now costs money either way — trim startup work or use SnapStart.
|
|
34
34
|
- **EF Core `Database.Migrate()` default command timeout = 30s.** Heavy migrations fail with EF timeout, not Lambda timeout. Set `CommandTimeout` explicitly.
|
|
35
35
|
|
|
36
36
|
## Function URL
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: browser-verify
|
|
3
|
+
description: Browser-automation verification gotchas — stale captures, caches, headless limits
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
## Browser-Automation Verification Gotchas
|
|
7
|
+
|
|
8
|
+
Traps when verifying web/canvas apps through Playwright or a browser MCP.
|
|
9
|
+
Common theme: **the automated browser is not the user's browser** — its
|
|
10
|
+
pixels, caches, and performance all lie in specific, repeatable ways.
|
|
11
|
+
|
|
12
|
+
### 1. Headless screenshot / `canvas.drawImage()` can return stale frames for a WebGL canvas
|
|
13
|
+
|
|
14
|
+
Animated content driven by per-frame GPU updates (custom shader uniforms, skeletal animations, shader-displaced vertices) may show **zero pixel diff** between headless screenshots taken 100s of ms apart, even when JS-side instrumentation confirms the state is updating each frame (RAF firing, uniform buffers advancing, ticker handlers incrementing). Sampling the canvas through `tmp.getContext('2d').drawImage(canvas, ...)` returns the same byte-for-byte image across samples — the whole canvas is affected, not just one animation.
|
|
15
|
+
|
|
16
|
+
Likely cause: with `preserveDrawingBuffer: false` (the default in most WebGL frameworks, incl. Pixi), the headless compositor reads from a cached present frame rather than the live WebGL back buffer.
|
|
17
|
+
|
|
18
|
+
- Don't rely on headless pixel-diff to verify GPU-driven animation works.
|
|
19
|
+
- Verify in a real browser tab (the user's running Chrome, not an automated instance).
|
|
20
|
+
- For automated verification, prefer JS-side state assertions: a uniform buffer value advancing across samples is sufficient proof the upload pipeline is alive; visual verification needs human eyes or a non-headless captured video.
|
|
21
|
+
|
|
22
|
+
### 2. Headed screenshots can ALSO be stale — a tab that was never frontmost composites its last presented frame
|
|
23
|
+
|
|
24
|
+
The same failure occurs HEADED whenever the target tab/window is occluded or was created behind another tab: `page.screenshot()` returns the last frame the browser's compositor ever presented for that tab. Observed: the screenshot showed a boot preloader frozen at "96%" while JS-side numeric sampling in the SAME page proved the app was live (object positions advancing every 100 ms, game logs firing). DOM inspection confirmed the preloader element was long gone — the "96%" pixels existed only in the stale compositor frame.
|
|
25
|
+
|
|
26
|
+
A second page created with `ctx.newPage()` and driven while another tab holds focus reproduces this; a page brought to front BEFORE its WebGL content started presenting captures real frames fine.
|
|
27
|
+
|
|
28
|
+
- Wrong: `ctx.newPage()` for a background capture tab, drive it, screenshot it while another tab stays focused — canvas pixels freeze at whatever was on screen when it lost focus (or never had it).
|
|
29
|
+
- Right:
|
|
30
|
+
- Reuse the context's initial tab (`ctx.pages()[0]`) rather than stacking `newPage()`s for capture work.
|
|
31
|
+
- Call `page.bringToFront()` immediately after `goto`, BEFORE the canvas first presents — not right before the screenshot.
|
|
32
|
+
- Treat JS-side numeric sampling (positions, probe state) as ground truth — it stays correct even when pixels are stale.
|
|
33
|
+
- For A/B visual comparisons, run each variant in its own sequential frontmost tab, never two tabs alternating.
|
|
34
|
+
|
|
35
|
+
### 3. Playwright MCP browser reuses a PERSISTENT cache across sessions — it can serve STALE js modules / outdated app logic
|
|
36
|
+
|
|
37
|
+
The Playwright MCP browser keeps a persistent profile + HTTP cache between conversations (`~/Library/Caches/ms-playwright-mcp/...` on macOS). A plain `browser_navigate` does NOT clear it, so when verifying app behavior against a dev server (e.g. Vite), the MCP browser can run **stale js modules** (a previous session's app-logic module) while the server already serves current code. The same seed/input then produces a DIFFERENT outcome in the MCP browser vs the user's real browser — and you'll confidently report the wrong result.
|
|
38
|
+
|
|
39
|
+
- Symptom: same dev server, same input, **different console result logs** in your browser vs the user's; your "verified" outcome contradicts what the user sees on screen.
|
|
40
|
+
- Red herring: a differing console source-LINE for the same log (e.g. playwright reports `logic.ts:103`, user's DevTools shows `:297`) is NOT a version mismatch — playwright reports the *transformed-module* line, DevTools the *sourcemapped source* line. Same code.
|
|
41
|
+
- Ground truth = the LIVE console log of the actual run in a FRESH browser, AND the user's real browser. NEVER a scanner-only prediction: a scanner's dynamic `import()` can be cached independently of the static import the live app uses, so the two disagree.
|
|
42
|
+
- Fix when your result conflicts with the user's: suspect YOUR cache first. `browser_close` then re-navigate relaunches a fresh browser instance — clears in-memory state and stale module instances, often enough against a dev server — but the default persistent mode reuses the same on-disk profile, so the HTTP disk cache/cookies survive. Guaranteed clean: run the MCP server with `--isolated`, point `--user-data-dir` at a throwaway dir, or delete the profile dir (`~/Library/Caches/ms-playwright-mcp/mcp-<channel>-<hash>` on macOS).
|
|
43
|
+
- `tsc`/`vitest` run via node against DISK code, so they stay valid for code correctness; only the BROWSER render/logic can be cache-stale. Re-verify any live finding in a freshly-cleared browser before claiming it.
|
|
44
|
+
- Cost when ignored: ~6 user round-trips insisting an input produced X while the user kept seeing Y — the user was right; the automated browser was stale.
|
|
45
|
+
|
|
46
|
+
### 4. Headless WebKit/Chromium on a desktop machine is NOT GPU-bound — useless for measuring MOBILE GPU perf deltas
|
|
47
|
+
|
|
48
|
+
When optimizing a WebGL scene for mobile/Safari GPU cost (resolution cap, MSAA, blend-mode overdraw, fill-rate), driving it with Playwright headless WebKit (or Chromium) on a desktop gives a **flat ~60 fps regardless of the renderer settings** — the desktop GPU absorbs pixel counts a phone can't. Observed: identical frame-time distribution (mean ~16.7 ms, p50 17, p99 18, 0 frames >33 ms) across BOTH `resolution=2` + MSAA on AND `resolution=1.5` + MSAA off, in the same scene. A headless FPS A/B between renderer configs shows **~0 delta by construction** and tells you nothing about the on-device win.
|
|
49
|
+
|
|
50
|
+
- Don't conclude "the optimization didn't help" from a headless FPS A/B — the bottleneck isn't reproduced.
|
|
51
|
+
- Measure the mobile GPU win **analytically** instead: device-pixel math (`resolution` 2.0→1.5 ⇒ `(1.5/2)² = 0.5625` ⇒ ~44% fewer fragment-shader invocations/frame), plus draw-call / additive-blend (overdraw) counting.
|
|
52
|
+
- Verify **correctness** directly: confirm the mobile code path APPLIED — e.g. read effective resolution as `canvas.width / parseFloat(canvas.style.width)` and expect the cap (1.5), not the raw `devicePixelRatio` (3) — and assert gating/visibility logic toggles via state, not via a screenshot pixel-diff.
|
|
53
|
+
- Trigger a mobile context so device-detection fires: `browser.newContext({ ...devices['iPhone 13'] })` gives iPhone UA + touch + DPR 3 + isMobile — but the ENGINE stays whatever you launched (the descriptor's `defaultBrowserType: 'webkit'` is honored only by the Playwright Test runner); launch `webkit` yourself to actually test WebKit.
|
|
54
|
+
- Ground truth for absolute on-device FPS is a physical phone, which Playwright cannot drive.
|
|
55
|
+
|
|
56
|
+
### Framework-specific siblings
|
|
57
|
+
|
|
58
|
+
Rendering-framework-specific verification recipes (freezing a scene's tickers for a deterministic screenshot, sampling motion on the render ticker instead of your own rAF) live in the domain skills — e.g. `domain-pixi` for Pixi.js.
|
|
@@ -7,24 +7,37 @@ description: CMake build system gotchas
|
|
|
7
7
|
|
|
8
8
|
## find_package Search Order
|
|
9
9
|
|
|
10
|
-
**No mode specified = tries MODULE first, then CONFIG** (
|
|
10
|
+
**No mode specified = tries MODULE first, then CONFIG** (commonly reversed!):
|
|
11
11
|
- First looks for `FindFoo.cmake` in CMAKE_MODULE_PATH (MODULE)
|
|
12
12
|
- Then falls back to `FooConfig.cmake` (CONFIG)
|
|
13
13
|
- `find_package(Foo)` might find wrong file — always specify CONFIG or MODULE explicitly
|
|
14
14
|
|
|
15
15
|
```cmake
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
# CONFIG: CMAKE_PREFIX_PATH entries are PREFIXES — CMake searches
|
|
17
|
+
# <prefix>/lib/cmake/Foo/, <prefix>/share/cmake/Foo/, etc.
|
|
18
|
+
# for FooConfig.cmake or foo-config.cmake (not the prefix dir itself)
|
|
19
|
+
set(CMAKE_PREFIX_PATH "/opt/mylib")
|
|
20
|
+
|
|
21
|
+
# CONFIG: Foo_DIR points at the EXACT dir containing FooConfig.cmake
|
|
22
|
+
set(Foo_DIR "/opt/mylib/lib/cmake/Foo")
|
|
23
|
+
|
|
24
|
+
# MODULE: CMAKE_MODULE_PATH dirs are searched DIRECTLY for FindFoo.cmake — NOT the same!
|
|
25
|
+
set(CMAKE_MODULE_PATH "/opt/cmake-modules")
|
|
18
26
|
```
|
|
19
27
|
|
|
20
|
-
## CMP0077: Normal Variables Override Options
|
|
28
|
+
## CMP0077: Normal Variables Override Options (add_subdirectory)
|
|
21
29
|
|
|
22
30
|
```cmake
|
|
23
|
-
|
|
24
|
-
set(FOO_ENABLE_TESTS
|
|
25
|
-
|
|
31
|
+
# Parent project configures a vendored subproject:
|
|
32
|
+
set(FOO_ENABLE_TESTS OFF) # normal variable — must be set BEFORE the option() executes
|
|
33
|
+
add_subdirectory(foo) # foo's CMakeLists.txt contains: option(FOO_ENABLE_TESTS "..." ON)
|
|
26
34
|
```
|
|
27
35
|
|
|
36
|
+
- CMP0077 **OLD**: `option()` discards the pre-set normal variable and creates the cache entry → the parent's `OFF` is lost.
|
|
37
|
+
- CMP0077 **NEW**: `option()` becomes a no-op when a normal variable of that name exists → the parent's `OFF` wins.
|
|
38
|
+
|
|
39
|
+
The policy is controlled by the **subproject**: `cmake_minimum_required(VERSION 3.13...)` there, or `cmake_policy(SET CMP0077 NEW)` before its `option()` calls. The parent can force it for all subprojects with `set(CMAKE_POLICY_DEFAULT_CMP0077 NEW)` before `add_subdirectory`.
|
|
40
|
+
|
|
28
41
|
Key: this is about **normal** variables overriding `option()` cache entries — not cache-to-cache override.
|
|
29
42
|
|
|
30
43
|
## Ubuntu apt catch2 = v2, NOT v3
|
|
@@ -54,5 +67,8 @@ endif()
|
|
|
54
67
|
FetchContent_Declare(Foo GIT_REPOSITORY https://github.com/org/foo.git GIT_TAG v1.0)
|
|
55
68
|
|
|
56
69
|
# GOOD (no git needed):
|
|
57
|
-
FetchContent_Declare(Foo
|
|
70
|
+
FetchContent_Declare(Foo
|
|
71
|
+
URL https://github.com/org/foo/archive/refs/tags/v1.0.tar.gz
|
|
72
|
+
URL_HASH SHA256=<tarball-sha256> # pin integrity — without it the download is unverified
|
|
73
|
+
)
|
|
58
74
|
```
|
|
@@ -18,14 +18,14 @@ description: GitHub Actions workflow gotchas
|
|
|
18
18
|
The action resolves the most recent tag **reachable from the current commit** via git history. With the default `fetch-depth: 1`, no tags are reachable — the action returns empty or stale results.
|
|
19
19
|
|
|
20
20
|
```yaml
|
|
21
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@v7
|
|
22
22
|
with:
|
|
23
23
|
fetch-depth: 0 # required for tag resolution
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
Same applies to `git describe`, `git log --follow`, `gitversion`, and any changelog generator that walks commit history.
|
|
27
27
|
|
|
28
|
-
## `actions/checkout
|
|
28
|
+
## `actions/checkout` Default Fetches Only 1 Commit (v4+)
|
|
29
29
|
|
|
30
30
|
Unless `fetch-depth: 0` (full history) or `fetch-depth: N` is set, you get a shallow clone with depth 1. Breaks: tag lookup, `git log`-based changelogs, `git blame`, branch comparison diffs.
|
|
31
31
|
|
|
@@ -53,10 +53,12 @@ deploy:
|
|
|
53
53
|
# RIGHT — run regardless, check status explicitly
|
|
54
54
|
deploy:
|
|
55
55
|
needs: [build]
|
|
56
|
-
if:
|
|
56
|
+
if: ${{ !cancelled() && needs.build.result == 'success' }}
|
|
57
57
|
steps: ...
|
|
58
58
|
```
|
|
59
59
|
|
|
60
|
+
Prefer `!cancelled()` over `always()`: `always()` forces the job to run even when the workflow run is being cancelled; `!cancelled()` lifts the "dependency failed/skipped" gate but still respects cancellation. (The `${{ }}` is required here — a bare `if:` starting with `!` is invalid YAML.)
|
|
61
|
+
|
|
60
62
|
## `$GITHUB_ENV` Changes Are Not Visible in the Same Step
|
|
61
63
|
|
|
62
64
|
`echo "X=value" >> $GITHUB_ENV` exports `X` for all **subsequent** steps. The current step cannot read it.
|
|
@@ -99,46 +101,60 @@ jobs:
|
|
|
99
101
|
test:
|
|
100
102
|
continue-on-error: true # whole matrix ignores failures
|
|
101
103
|
|
|
102
|
-
#
|
|
103
|
-
strategy:
|
|
104
|
-
matrix:
|
|
105
|
-
os: [ubuntu, windows]
|
|
106
|
-
fail-fast: false
|
|
104
|
+
# Cell-level — only the flagged cell continues, others still gate the job
|
|
107
105
|
jobs:
|
|
108
106
|
test:
|
|
107
|
+
strategy:
|
|
108
|
+
fail-fast: false
|
|
109
|
+
matrix:
|
|
110
|
+
os: [ubuntu, windows]
|
|
109
111
|
continue-on-error: ${{ matrix.os == 'windows' }} # only windows cell is optional
|
|
110
112
|
```
|
|
111
113
|
|
|
114
|
+
Note `strategy:` lives under `jobs.<job>` — it is not a top-level workflow key.
|
|
115
|
+
|
|
112
116
|
`fail-fast: false` (strategy level) keeps sibling cells running after one fails. `continue-on-error: true` (job level) prevents the job from being marked failed. They're orthogonal.
|
|
113
117
|
|
|
114
|
-
## Artifact Names Must Be Unique Within a Run (actions/upload-artifact
|
|
118
|
+
## Artifact Names Must Be Unique Within a Run (actions/upload-artifact v4+)
|
|
115
119
|
|
|
116
|
-
v4
|
|
120
|
+
v4 made artifacts immutable — uploading two artifacts with the same name in one workflow run is an error (v3 silently merged files into one artifact). Since v4.4 an explicit `overwrite: true` input deletes and replaces the existing artifact; the default is still an error.
|
|
117
121
|
|
|
118
122
|
```yaml
|
|
119
123
|
# WRONG — both matrix cells upload "test-results", second one errors
|
|
120
|
-
- uses: actions/upload-artifact@
|
|
124
|
+
- uses: actions/upload-artifact@v7
|
|
121
125
|
with:
|
|
122
126
|
name: test-results
|
|
123
127
|
|
|
124
128
|
# RIGHT — include matrix dimension in the name
|
|
125
|
-
- uses: actions/upload-artifact@
|
|
129
|
+
- uses: actions/upload-artifact@v7
|
|
126
130
|
with:
|
|
127
131
|
name: test-results-${{ matrix.os }}
|
|
128
132
|
```
|
|
129
133
|
|
|
130
|
-
## `if:` Expressions
|
|
134
|
+
## `if:` Expressions: Single Quotes Only, Comparison Is Case-INsensitive
|
|
131
135
|
|
|
132
136
|
```yaml
|
|
133
|
-
# WRONG —
|
|
134
|
-
if: github.event_name == "
|
|
137
|
+
# WRONG — double quotes are a validation error in expressions
|
|
138
|
+
if: github.event_name == "push"
|
|
135
139
|
|
|
136
140
|
# RIGHT
|
|
137
141
|
if: github.event_name == 'push'
|
|
138
142
|
```
|
|
139
143
|
|
|
140
|
-
|
|
144
|
+
String literals in expressions must use single quotes — double quotes fail workflow validation. String comparison with `==` ignores case (`'Push' == 'push'` is true). The operator is `==`; there is no `===`.
|
|
145
|
+
|
|
146
|
+
`if:` expressions do NOT require `${{ }}` wrapping (they're evaluated as expressions already). Adding `${{ }}` works but is redundant — except when the expression starts with `!` (reserved in YAML), e.g. `if: ${{ !cancelled() }}`.
|
|
147
|
+
|
|
148
|
+
## `env:` Does Not Propagate Into Reusable Workflow Calls (`jobs.<job>.uses`)
|
|
149
|
+
|
|
150
|
+
A reusable workflow is called at the JOB level (`jobs.<job>.uses`), not as a step. Environment variables set at workflow or job level are available to `run:` steps but are NOT passed into the called reusable workflow. Pass values explicitly via `with:` inputs or `secrets:` instead.
|
|
151
|
+
|
|
152
|
+
## Default Community Health Files (FUNDING.yml, CODEOWNERS, ISSUE_TEMPLATE) Load ONLY From a Repo Named `.github`
|
|
153
|
+
|
|
154
|
+
Account-wide defaults for community health files come **only** from a public repo named literally `.github` (e.g. `owner/.github`), in its root or a `.github`/`docs` folder. A repo's own file overrides the default.
|
|
155
|
+
|
|
156
|
+
**Wrong:** Putting `FUNDING.yml` in the username/profile repo (`owner/owner`, the one with the profile README) expecting it to apply org-wide — it does NOT. It only affects that single repo.
|
|
141
157
|
|
|
142
|
-
|
|
158
|
+
**Right:** Create `owner/.github` (must be public) → its `FUNDING.yml` gives every repo without its own a Sponsor button.
|
|
143
159
|
|
|
144
|
-
|
|
160
|
+
`FUNDING.yml` keys are platform-specific usernames, not URLs: `buy_me_a_coffee: <user>`, `patreon: <user>`. There is **no** `paypal:` key — PayPal goes under `custom: ['https://paypal.me/<user>']`.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-code-config
|
|
3
|
+
description: Claude Code configuration gotchas — permission rule syntax and evaluation order, settings hierarchy, plugin install scopes, hook behavior
|
|
3
4
|
---
|
|
4
5
|
|
|
5
6
|
# Claude Code Configuration Gotchas
|
|
@@ -14,9 +15,11 @@ name: claude-code-config
|
|
|
14
15
|
**Common mistake**: Writing `Read(/tmp/**)` when you mean absolute path. Correct: `Read(//tmp/**)`.
|
|
15
16
|
|
|
16
17
|
**Pattern format must match tool signature**:
|
|
17
|
-
- Correct: `Read(//tmp/**)`, `Bash(npm run
|
|
18
|
+
- Correct: `Read(//tmp/**)`, `Bash(npm run:*)`
|
|
18
19
|
- Wrong: `Read(file_path://tmp/**)` — tool name only, no parameter labels
|
|
19
20
|
|
|
21
|
+
**Bash rules use `:*` for prefix matching**: `Bash(npm run:*)` matches any command starting with `npm run `. `Bash(npm run *)` (space-star) is NOT the prefix syntax and matches unreliably.
|
|
22
|
+
|
|
20
23
|
## Wildcard Matching
|
|
21
24
|
|
|
22
25
|
**Recursive vs Single Level**:
|
|
@@ -29,8 +32,8 @@ name: claude-code-config
|
|
|
29
32
|
|
|
30
33
|
**Deny always wins**:
|
|
31
34
|
1. Deny rules checked first (block regardless of anything else)
|
|
32
|
-
2.
|
|
33
|
-
3.
|
|
35
|
+
2. Ask rules checked second (prompt user if matched)
|
|
36
|
+
3. Allow rules checked last (permit if matched)
|
|
34
37
|
|
|
35
38
|
**First match wins** within each category.
|
|
36
39
|
|
|
@@ -48,12 +51,8 @@ name: claude-code-config
|
|
|
48
51
|
|
|
49
52
|
## Known Bugs (as of 2026)
|
|
50
53
|
|
|
51
|
-
**Deny rules not enforced**: Multiple reported issues (GH #13785, #6631, #4467, #6699) where deny patterns in settings.json are ignored. Tool still gets access despite explicit deny.
|
|
52
|
-
|
|
53
54
|
**Allow rules ignored**: Issue #18160 reports global allow permissions being ignored, requiring manual approval despite being in allowlist.
|
|
54
55
|
|
|
55
|
-
**Wildcard pattern bugs**: `Bash(command *)` patterns may fail to match commands with flags or tilde expansion.
|
|
56
|
-
|
|
57
56
|
## Domain-Specific Rules
|
|
58
57
|
|
|
59
58
|
**WebFetch requires domain syntax**:
|
|
@@ -64,9 +63,7 @@ name: claude-code-config
|
|
|
64
63
|
|
|
65
64
|
**Overly broad wildcards**: Starting with `Bash(*)` or `Read(**)` creates security holes. Be specific.
|
|
66
65
|
|
|
67
|
-
**
|
|
68
|
-
|
|
69
|
-
**Environment variables in Bash**: Don't persist between commands. Use `CLAUDE_ENV_FILE` or hooks instead.
|
|
66
|
+
**Environment variables in Bash**: Don't persist between commands. Use the `env` map in settings.json or hooks instead.
|
|
70
67
|
|
|
71
68
|
**Testing permissions**: Use Shift+Tab to switch permission modes mid-session without editing files.
|
|
72
69
|
|
|
@@ -76,16 +73,16 @@ name: claude-code-config
|
|
|
76
73
|
{
|
|
77
74
|
"permissions": {
|
|
78
75
|
"allow": [
|
|
79
|
-
"Read(
|
|
76
|
+
"Read(./config.json)", // relative: config.json in settings file dir
|
|
80
77
|
"Read(//tmp/**)", // absolute: all files in /tmp
|
|
81
78
|
"Read(~/.config/**)", // home: all files in ~/.config
|
|
82
|
-
"Read(
|
|
83
|
-
"Bash(npm run
|
|
79
|
+
"Read(**/config.json)", // recursive: config.json anywhere in subtree
|
|
80
|
+
"Bash(npm run:*)", // command prefix matching (:* suffix)
|
|
84
81
|
"WebFetch(domain:docs.anthropic.com)" // domain-specific
|
|
85
82
|
],
|
|
86
83
|
"deny": [
|
|
87
84
|
"Read(**/secrets/**)", // block entire directory tree
|
|
88
|
-
"Bash(rm
|
|
85
|
+
"Bash(rm:*)" // block dangerous commands
|
|
89
86
|
]
|
|
90
87
|
}
|
|
91
88
|
}
|
|
@@ -94,14 +91,14 @@ name: claude-code-config
|
|
|
94
91
|
## Debugging Permission Issues
|
|
95
92
|
|
|
96
93
|
1. Check hierarchy: local > project > user > enterprise
|
|
97
|
-
2. Check deny
|
|
94
|
+
2. Check rule order: deny → ask → allow (deny always wins)
|
|
98
95
|
3. Test pattern manually: Does it match the exact tool signature shown in prompt?
|
|
99
|
-
4. Check for known bugs (
|
|
96
|
+
4. Check for known bugs (allow rules may be ignored — #18160)
|
|
100
97
|
5. Use `claude mcp list` to verify MCP server status if permission involves MCP tool
|
|
101
98
|
|
|
102
99
|
## Plugin Install Scope
|
|
103
100
|
|
|
104
|
-
`claude
|
|
101
|
+
`claude plugin install <pkg>@<marketplace>` (singular `plugin`, not `plugins`) takes `-s, --scope <user|project|local>` (default: `user`).
|
|
105
102
|
|
|
106
103
|
- `user` — global, ~/.claude/plugins/installed_plugins.json with `"scope": "user"`
|
|
107
104
|
- `project` — must run from within the target project dir; registry entry gets `"scope": "project"` + `"projectPath": "<cwd>"`. Plugin activates only when Claude Code runs inside that dir
|
|
@@ -109,14 +106,39 @@ name: claude-code-config
|
|
|
109
106
|
|
|
110
107
|
Swap scope = uninstall + reinstall (no in-place conversion):
|
|
111
108
|
```bash
|
|
112
|
-
claude
|
|
113
|
-
cd /path/to/project && claude
|
|
109
|
+
claude plugin uninstall <pkg>@<mkt> --scope user
|
|
110
|
+
cd /path/to/project && claude plugin install <pkg>@<mkt> --scope project
|
|
114
111
|
```
|
|
115
112
|
|
|
116
113
|
All scopes share the same `~/.claude/plugins/cache/` payload — only the registry entry differs.
|
|
117
114
|
|
|
115
|
+
## Stop Hook Fires Per Turn, Not on Process Exit
|
|
116
|
+
|
|
117
|
+
**Wrong assumption**: `Stop` hook = "CLI process is shutting down / session ending".
|
|
118
|
+
**Correct**: `Stop` fires **each time the main agent finishes responding** (end of every turn). One CLI process lifetime can trigger it dozens of times. Docs: *"when Claude finishes responding"* — [docs.claude.com/en/docs/claude-code/hooks](https://docs.claude.com/en/docs/claude-code/hooks).
|
|
119
|
+
|
|
120
|
+
`SubagentStop` fires when a subagent (Task tool) finishes — separate from the main agent's Stop.
|
|
121
|
+
|
|
122
|
+
**Practical implication**: Any state registered on `UserPromptSubmit` and released on `Stop` churns every turn. For per-process state (e.g. a background daemon, a lock file, an IPC socket) key by **CLI PID** — `session_id` is wrong because it also changes on `/clear`, `/compact`, or resume within the same process.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
UserPromptSubmit → fires before each user turn
|
|
126
|
+
Stop → fires after each assistant response (not at exit)
|
|
127
|
+
SubagentStop → fires when a Task-tool subagent completes
|
|
128
|
+
PostToolUse → fires after each individual tool call within a turn
|
|
129
|
+
```
|
|
130
|
+
|
|
118
131
|
## npx Argument Re-parsing Under PreToolUse Hooks
|
|
119
132
|
|
|
120
133
|
PreToolUse Bash hooks that rewrite commands can mis-parse `npx -y <pkg>@latest <subcmd>`, with `<pkg>@latest` landing as an **npm** subcommand, producing `Unknown command: "<pkg>@latest"`.
|
|
121
134
|
|
|
122
135
|
Workaround: invoke through a wrapper that bypasses the rewriter (e.g. an explicit `proxy` subcommand of the hook tool, or call the binary directly without `npx -y`). The same `npx` call may succeed via workflow-engine exec while failing via Bash tool — the difference is whether the hook intercepts.
|
|
136
|
+
|
|
137
|
+
## Multiple Hook Entries for the Same Event Run in Parallel
|
|
138
|
+
|
|
139
|
+
**Wrong assumption**: Two `PreToolUse` entries in `hooks.json` run in declaration order — first entry first, second entry second.
|
|
140
|
+
**Correct**: All entries for the same event fire **simultaneously**. Declaration order is not execution order.
|
|
141
|
+
|
|
142
|
+
**Race-condition trap**: If entry A writes a marker file and entry B deletes it, the outcome depends on timing, not position. A fast B (~0 ms) completes before a slow A (~30–50 ms), so A's delete wins even when B is declared last. Symptom: your write appears to have no effect.
|
|
143
|
+
|
|
144
|
+
**Fix**: One hook entry per event. Inspect stdin JSON inside that single script/command and branch on `tool_name` or other fields to dispatch logic. This guarantees deterministic ordering because it's a single process.
|