> Rules for writing and reviewing GC-safe C++ code in the Hermes VM runtime. Use when writing, modifying, or reviewing C++ runtime VM code that uses internal Hermes VM APIs (as opposed to code using JSI). This includes working with GC-managed types (HermesValue, Handle, PinnedValue, JSObject, StringPrimitive, etc.), Locals, GCScope, PseudoHandle, CallResult, or any function with _RJS suffix. Typically in lib/VM/, include/hermes/VM/, API/hermes/, or API/napi/.
npx skills add https://github.com/facebook/hermes --skill gc-safe-coding
For the full explanation and rationale, see doc/GCSafeCoding.md.
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>.
All new code must use Locals + PinnedValue<T>. Do not introduce new
GCScope instances or makeHandle() calls.
struct : public Locals {
PinnedValue<JSObject> obj;
PinnedValue<StringPrimitive> str;
PinnedValue<SymbolID> sym;
PinnedValue<> genericValue;
} lv;
LocalsRAII lraii(runtime, &lv);
lv.obj = std::move(*callResult);lv.obj.castAndSetHermesValue<JSObject>(hv);lv.obj = somePtr;lv.obj = nullptr;lv.obj.template castAndSetHermesValue<T>(hv);PinnedValue<T> implicitly converts to Handle<T>. Pass directly to functions
that accept Handle<T>.
Always check for exceptions before using the value:
auto result = someOperation_RJS(runtime, args);
if (LLVM_UNLIKELY(result == ExecutionStatus::EXCEPTION))
return ExecutionStatus::EXCEPTION;
lv.obj = std::move(*result);
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 doesnot 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 registerstack, which is already a root. No new allocation.
Handle<> parameter — the handle was allocated bythe 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 &).
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>.
GCScope ormakeHandle(). Declare a struct : public Locals with PinnedValue fields
and a LocalsRAII.
CallResult without firstchecking == ExecutionStatus::EXCEPTION.
Handle<T> pointinginto a PinnedValue or GCScope that is about to be destroyed. Return
CallResult<PseudoHandle<T>> or CallResult<HermesValue> instead.
before calling castAndSetHermesValue.
PinnedValue fields are reused eachiteration — no unbounded growth. If a GCScope is still needed for legacy
APIs that return Handle, use GCScopeMarkerRAII or flushToMarker.
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.
flushToMarker invalidates handles allocated after the marker. Anyvalue 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.
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.
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
Automatically creates user-facing changelogs from git commits by analyzing commit history, categorizing changes, and transforming technical commits into clear, customer-friendly release notes. Turns hours of manual changelog writing into minutes of automated generation.
Use when implementation is complete, all tests pass, and you need to decide how to integrate the work - guides completion of development work by presenting structured options for merge, PR, or cleanup
Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node/TypeScript (MCP SDK).
React Native and Expo best practices for building performant mobile apps. Use when building React Native components, optimizing list performance, implementing animations, or working with native modules. Triggers on tasks involving React Native, Expo, mobile performance, or native platform APIs.
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling
Use when starting feature work that needs isolation from current workspace or before executing implementation plans - creates isolated git worktrees with smart directory selection and safety verification
Take facebook/gc-safe-coding from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.