gc-safe-coding
For the full explanation and rationale, see doc/GCSafeCoding.md.
Works with
Agent Skills format with YAML frontmatter. Claude Code reads it as-is.
---
name: "gc-safe-coding"
description: "For the full explanation and rationale, see doc/GCSafeCoding.md."
license: "MIT"
---
For the full explanation and rationale, see [doc/GCSafeCoding.md](doc/GCSafeCoding.md).
## GC safepoints
A GC safepoint is either a GC heap allocation or a function call that might
transitively reach one (regular C heap allocations like `malloc` are not
safepoints). Any function that takes `Runtime &` or `PointerBase &`
may trigger GC, unless documented otherwise or named with `_noalloc`/`_nogc`.
Functions with `_RJS` suffix invoke JavaScript recursively and always trigger
GC.
**All raw pointers and PseudoHandles to GC objects must be rooted before any
GC safepoint.** `PseudoHandle<T>` is *not* a root — it is just as dangerous as
a raw pointer across a safepoint. The same applies to bare `SymbolID` values
extracted from a non-uniqued source (e.g., the `SymbolID` pulled out of the
`Handle<SymbolID>` returned by `getSymbolHandleFromPrimitive` for a
freshly-allocated `StringPrimitive`): once nothing roots it, the lookup-table
slot is reclaimed by `freeUnmarkedSymbols` during sweep. Pin via
`PinnedValue<SymbolID>`.
## Rooting local values: use Locals + PinnedValue (required for new code)
All new code must use `Locals` + `PinnedValue<T>`. Do not introduce new
`GCScope` instances or `makeHandle()` calls.
```cpp
struct : public Locals {
PinnedValue<JSObject> obj;
PinnedValue<StringPrimitive> str;
PinnedValue<SymbolID> sym;
PinnedValue<> genericValue;
} lv;
LocalsRAII lraii(runtime, &lv);
```
### Assignment patterns
- **From PseudoHandle:** `lv.obj = std::move(*callResult);`
- **From HermesValue with known type:** `lv.obj.castAndSetHermesValue<JSObject>(hv);`
- **From raw pointer:** `lv.obj = somePtr;`
- **Clear:** `lv.obj = nullptr;`
- **In template context:** `lv.obj.template castAndSetHermesValue<T>(hv);`
### Passing to functions
`PinnedValue<T>` implicitly converts to `Handle<T>`. Pass directly to functions
that accept `Handle<T>`.
## Error handling with CallResult
Always check for exceptions before using the value:
```cpp
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.obj = std::move(*result);
```
## When Handle usage is fine (do not flag)
Not every use of `Handle<>` needs to be converted to `PinnedValue`. The rule
"use Locals, not GCScope" applies to **creating new rooted values** — allocating
new `PinnedHermesValue` slots via `makeHandle()` or `Handle<>` constructors.
The following are **not** allocating new handles and do not need conversion:
- **`vmcast<>(handle)`** — casts an existing handle to a different type. It does
not take `Runtime &` and does not allocate a GCScope slot. The result points
to the same `PinnedHermesValue` as the input.
- **`args.getArgHandle(n)`** — returns a handle pointing into the register
stack, which is already a root. No new allocation.
- **Passing or receiving a `Handle<>` parameter** — the handle was allocated by
the caller; the callee is just using it.
Only flag handle usage when a **new** `PinnedHermesValue` slot is being
allocated (via `makeHandle()`, `makeMutableHandle()`, or `Handle<>`/
`MutableHandle<>` constructors that take `Runtime &`).
## Checklist for writing / reviewing GC-safe code
1. **No raw pointers or PseudoHandles across GC safepoints.** Every pointer to
a GC object — including values held in `PseudoHandle<T>` — must be stored in
a `PinnedValue` before any call that takes `Runtime &` or is `_RJS`.
Watch for multi-step creation patterns: if `Foo::create()` returns a
`PseudoHandle` and the next line calls `Bar::create(runtime)`, the first
`PseudoHandle` is stale after the second allocation.
Equally watch for capture-via-deref: `auto *x = vmcast<T>(*pinned)` extracts
a raw pointer from a pinned location (e.g., a `PinnedHermesValue *` such as
a `napi_value`). The pinned slot stays GC-safe, but the local raw pointer
does not. Re-deref `*pinned` at each use site, or pin via `PinnedValue<T>`.
2. **Use Locals, not GCScope.** New code must not introduce `GCScope` or
`makeHandle()`. Declare a `struct : public Locals` with `PinnedValue` fields
and a `LocalsRAII`.
3. **Check every CallResult.** Never dereference a `CallResult` without first
checking `== ExecutionStatus::EXCEPTION`.
4. **Never return Handle from local roots.** Do not return `Handle<T>` pointing
into a `PinnedValue` or `GCScope` that is about to be destroyed. Return
`CallResult<PseudoHandle<T>>` or `CallResult<HermesValue>` instead.
5. **Null prototype checks.** When traversing prototype chains, check for null
before calling `castAndSetHermesValue`.
6. **Loops are safe with Locals.** `PinnedValue` fields are reused each
iteration — no unbounded growth. If a `GCScope` is still needed for legacy
APIs that return `Handle`, use `GCScopeMarkerRAII` or `flushToMarker`.
7. **Handles allocate in the topmost GCScope.** `makeHandle()`,
`makeMutableHandle()`, `Handle<>` and `MutableHandle<>` constructors, and
calls to functions that take `Runtime &`/`PointerBase &` and return
`Handle<>`, all allocate a slot in the topmost `GCScope`. Functions that
create or receive handles without returning them need their own `GCScope` or
`GCScopeMarkerRAII` (preferred for one or two handles). Functions like
`vmcast<>` that do not take `Runtime &` just cast existing handles without
allocating.
8. **`flushToMarker` invalidates handles allocated after the marker.** Any
value extracted from such a Handle (raw pointer, bare `SymbolID`) is
unrooted after the flush. Pin into a `PinnedValue` *before* the flush if
the value is needed later.
## Debugging tips
- If `IdentifierTable::materializeLazyIdentifier` asserts
`(entry.isLazyASCII() || entry.isLazyUTF16()) && "identifier is not lazy"`,
the entry is most often a free-list slot — look up the call stack for an
unrooted `SymbolID` held across an allocation.More General & Other skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
grill-me
mattpocock/skills
A relentless interview to sharpen a plan or design.
grill-with-docs
mattpocock/skills
A relentless interview to sharpen a plan or design, which also creates docs (ADR's and glossary) as we go.

