Extend Commands API
redis/lettuce
Add or extend Redis commands in the Lettuce client API end-to-end — a new core command, a family of new commands, an extension to an existing command's options, or a module/area command…
Step-by-step guide for adding a new built-in RESP command to Garnet, from the command enum and parser to storage callbacks, command metadata JSON and tests.
$ npx skills add microsoft/garnet --skill add-garnet-command -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install microsoft/garnet add-garnet-command --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/microsoft/garnet.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.github/skills/add-garnet-command .claude/skills/add-garnet-command && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "add-garnet-command" agent skill from https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-command into .claude/skills/add-garnet-command/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-garnet-command", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-commandType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add microsoft/garnet --skill add-garnet-command -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install microsoft/garnet add-garnet-command --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/garnet.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.github/skills/add-garnet-command .agents/skills/add-garnet-command && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "add-garnet-command" agent skill from https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-command into .agents/skills/add-garnet-command/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-garnet-command", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add microsoft/garnet --skill add-garnet-command -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install microsoft/garnet add-garnet-command --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/garnet.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.github/skills/add-garnet-command .cursor/skills/add-garnet-command && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "add-garnet-command" agent skill from https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-command into .cursor/skills/add-garnet-command/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-garnet-command", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/microsoft/garnet.git --path .github/skills/add-garnet-command--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add microsoft/garnet --skill add-garnet-command -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install microsoft/garnet add-garnet-command --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/garnet.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.github/skills/add-garnet-command .gemini/skills/add-garnet-command && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "add-garnet-command" agent skill from https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-command into .gemini/skills/add-garnet-command/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-garnet-command", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install microsoft/garnet add-garnet-commandInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add microsoft/garnet --skill add-garnet-command -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/microsoft/garnet.git skills-src && mkdir -p .github/skills && cp -r skills-src/.github/skills/add-garnet-command .github/skills/add-garnet-command && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "add-garnet-command" agent skill from https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-command into .github/skills/add-garnet-command/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-garnet-command", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add microsoft/garnet --skill add-garnet-command -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install microsoft/garnet add-garnet-command --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/microsoft/garnet.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.github/skills/add-garnet-command .opencode/skills/add-garnet-command && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "add-garnet-command" agent skill from https://github.com/microsoft/garnet/tree/main/.github/skills/add-garnet-command into .opencode/skills/add-garnet-command/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "add-garnet-command", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
add-garnet-commandStep-by-step guide for adding a new built-in RESP command to Garnet, from the command enum and parser to storage callbacks, command metadata JSON and tests.
The skill lists every area that changes when a built-in command is added to the Garnet server: the RespCommand enum and parser in libs/server/Resp/Parser/RespCommand.cs, dispatch in RespServerSession.cs, a RESP handler file, the IGarnetApi interface and its delegation, the storage session, RMW and read callbacks, variable-length methods, object operations, and an item broker for blocking commands. A table marks which areas are always required and which apply only to key-value, string, object or blocking commands.
It also covers the generated command info and docs JSON in libs/resources, the CommandInfoUpdater files under playground (supported commands plus Garnet-only info and docs), ACL tests and integration tests, along with caveats found during implementation and how to verify correctness. Scope is limited to built-in commands, so custom extension commands such as CustomRawStringFunctions registered through REGISTERCS are left to other guidance.
12 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 653bb32. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
dotnetFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Add Garnet RESP Command loads about 9.7k tokens when it runs. Until then it costs about 109 tokens; SKILL.md has 3,336 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from microsoft/garnet at commit 653bb32, republished under its MIT licence (© microsoft). 3,336 words, ~9,717 tokens.
.claude/skills/add-garnet-command/SKILL.md (or your agent's skills folder).Step-by-step guide for implementing a new built-in RESP command in Garnet. This covers every file that must be created or modified, the tools that must be run, caveats discovered during implementation, and how to verify correctness.
Scope: Built-in commands that are part of the Garnet server (not custom extensions registered via REGISTERCS).
Adding a single new command touches at minimum these areas:
| # | Area | Files | Required? |
|---|---|---|---|
| 1 | RespCommand enum | libs/server/Resp/Parser/RespCommand.cs | ✅ Always |
| 2 | Command parsing | libs/server/Resp/Parser/RespCommand.cs | ✅ Always |
| 3 | Command dispatch | libs/server/Resp/RespServerSession.cs | ✅ Always |
| 4 | RESP handler | libs/server/Resp/<Category>/*.cs | ✅ Always |
| 5 | API interface | libs/server/API/IGarnetApi.cs | If key-value command (not blocking/admin) |
| 6 | API delegation | libs/server/API/GarnetApi*.cs | If key-value command (not blocking/admin) |
| 7 | Storage session | libs/server/Storage/Session/[Main|Object|Unified]Store/*Ops.cs | If key-value command (not blocking/admin) |
| 8 | RMW/Read callbacks | libs/server/Storage/Functions/[Main|Unified]Store/[RMW|Read]Methods.cs | If string/unified command using RMW/Read |
| 8b | Read response | libs/server/Storage/Functions/MainStore/PrivateMethods.cs | If string command using Read (add to CopyRespToWithInput) |
| 9 | VarLen methods | libs/server/Storage/Functions/[Main|Unified]Store/VarLenInputMethods.cs | If string/unified command using RMW |
| 10 | Object operation enum | libs/server/Objects/[ObjectName]/[ObjectName]Object.cs | If new object sub-operation |
| 11 | Object implementation | libs/server/Objects/[ObjectName]/[ObjectName]ObjectImpl.cs | If new object sub-operation |
| 12 | ItemBroker | libs/server/Objects/ItemBroker/CollectionItemBroker.cs | If blocking command |
| 13 | Command info JSON | libs/resources/RespCommandsInfo.json | ✅ Always (generated) |
| 14 | Command docs JSON | libs/resources/RespCommandsDocs.json | ✅ Always (generated) |
| 15 | Supported commands | playground/CommandInfoUpdater/SupportedCommand.cs | ✅ Always |
| 16 | Garnet command info | playground/CommandInfoUpdater/GarnetCommandsInfo.json | If Garnet-only command |
| 17 | Garnet command docs | playground/CommandInfoUpdater/GarnetCommandsDocs.json | If Garnet-only command |
| 18 | ACL test | test/Garnet.test/Resp/ACL/RespCommandTests.cs | ✅ Always |
| 19 | Integration tests | test/Garnet.test/Resp*.cs | ✅ Always |
| 20 | Website documentation | website/docs/commands/ | ✅ Always |
| 21 | Configuration settings | Options.cs, GarnetServerOptions.cs, defaults.conf | If command is optional/gated |
File: libs/server/Resp/Parser/RespCommand.cs
The RespCommand enum is writes-first: write (mutating) commands occupy a dense,
explicitly-numbered block immediately after NONE, followed by reads, scripts, and non-data:
Write commands: APPEND = 1 ... BITOP_DIFF = 120 (EXPLICIT values; the ONLY persisted values)
Read commands: BITCOUNT ... RISCAN (auto-numbered; never persisted)
Script commands: EVAL, EVALSHA (LastDataCommand = EVALSHA)
Non-key commands: PING, SUBSCRIBE, etc.
Admin commands: AUTH, CONFIG, etc.Why writes-first: the write command's numeric value is persisted to disk (serialized into
RespInputHeader.cmd in the AOF and streamed to replicas). Keeping writes in a dense,
explicitly-numbered leading block makes those values stable — adding a read/admin command
(which is never persisted) cannot shift them.
Read/write classification uses enum ranges (positional constants, not hardcoded values):
RespCommandExtensions.FirstWriteCommand (APPEND=1) .. LastWriteCommand (BITOP_DIFF=120) → writeFirstReadCommand .. LastReadCommand (= EVAL - 1) → read-onlyRules for adding a command:
// Read-only commands marker, with the next explicit value (e.g. MYCMD = 121,).
Then update LastWriteCommand if it is no longer BITOP_DIFF. Never insert into the middle
of the write block or change/reuse an existing value — persisted values must be append-only.Guardrails (do not bypass):
PersistedEnumStabilityTests snapshots the write-block values and asserts the write range
contains exactly the write commands. If you change an existing write value it fails — that is a
breaking on-disk change requiring an AofHeader.AofHeaderVersion bump + a legacy remap in
AofProcessor / LegacyRespCommand, not an edited expected value.0xFEFF), that is also
caught by a test.Boundary markers to watch (search for these comments):
APPEND = 1, // Note: FirstWriteCommand — keep as the first write command
BITOP_DIFF = 120, // Note: LastWriteCommand — append new write commands after this with the next value
EVALSHA, // Note: Update LastDataCommand if adding new data commands after thisFile: libs/server/Resp/Parser/RespCommand.cs
Two parsing tiers exist:
RespCommandHashLookup (primary path for most commands)The hash table in libs/server/Resp/Parser/RespCommandHashLookupData.cs provides O(1) lookup for all built-in commands. This is the recommended path for all new commands.
To add a new primary command, add one line to PopulatePrimaryTable() in RespCommandHashLookupData.cs:
Add("DELIFGREATER", RespCommand.DELIFGREATER);To add a command with subcommands (e.g., MYPARENT SUBCMD):
hasSub: true in PopulatePrimaryTable():Add("MYPARENT", RespCommand.MYPARENT, hasSub: true);RespCommandHashLookupData.cs:private static readonly (string Name, RespCommand Command)[] MyparentSubcommands =
[
("SUBCMD1", RespCommand.MYPARENT_SUBCMD1),
("SUBCMD2", RespCommand.MYPARENT_SUBCMD2),
];RespCommandHashLookup.cs:myparentSubTable = BuildSubTable(MyparentSubcommands, out myparentSubTableMask);LookupSubcommand() in RespCommandHashLookup.cs:RespCommand.MYPARENT => (myparentSubTable, myparentSubTableMask),⚠️ Important: Use the exact wire-protocol spelling for the hash table name string. Some commands use hyphens (e.g., "SET-CONFIG-EPOCH" not "SETCONFIGEPOCH"). Check CmdStrings.cs for the canonical spelling.
⚠️ Convention: Define the command name string in libs/server/Resp/CmdStrings.cs and reference it from the parser, rather than using inline "..."u8 literals. This keeps command name strings centralized and reusable (e.g., for error messages).
// In CmdStrings.cs:
public static ReadOnlySpan<byte> DELIFGREATER => "DELIFGREATER"u8;FastParseCommand() (optional, for hottest commands only)Static Vector128<byte> patterns defined in libs/server/Resp/Parser/RespCommandSimdPatterns.cs that match the full RESP encoding (*N\r\n$L\r\nCMD\r\n) in a single 16-byte comparison. Use the RespPattern(argCount, "CMD") helper to create new patterns. Only needed for the most performance-critical commands with:
Most new commands should NOT be added here — the hash table + MRU cache provide excellent performance for all commands. Only add SIMD patterns if benchmarking shows the command is a bottleneck.
File: libs/server/Resp/RespServerSession.cs
Three dispatch methods exist, and which one you use matters for latency tracking:
| Method | For | Latency |
|---|---|---|
ProcessBasicCommands | Fast single/dual-arg commands | @fast only |
ProcessArrayCommands | Fast multi-arg commands | @fast only |
ProcessOtherCommands | Slow commands, admin commands | @slow OK |
⚠️ WARNING: Do NOT add @slow-classified commands to ProcessBasicCommands or ProcessArrayCommands. This breaks latency tracking. If in doubt, use ProcessOtherCommands.
Pattern:
RespCommand.MYCMD => NetworkMYCMD(ref storageApi),Add before the _ => ... fallthrough in the appropriate method.
New file or existing file in libs/server/Resp/
Command handlers are methods on the RespServerSession partial class:
private bool NetworkMYCMD<TGarnetApi>(ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
// 1. Validate argument count
if (parseState.Count != N)
return AbortWithWrongNumberOfArguments(nameof(RespCommand.MYCMD));
// 2. Validate other inputs (short-circuit before going to storage)
var key = parseState.GetArgSliceByRef(0);
// e.g., parse and validate optional flags, numeric arguments, etc.
if (!parseState.TryGetInt(1, out var _))
{
WriteError(CmdStrings.RESP_ERR_GENERIC_VALUE_IS_NOT_INTEGER);
return true;
}
// 3. Build input/output and call storage API
// Note: To avoid double-parsing a parameter, you can pass a pre-parsed
// value in the input struct's auxiliary arguments (e.g., input.arg1).
var input = new StringInput(RespCommand.MYCMD, ref parseState, startIdx: 1);
var output = GetStringOutput();
var status = storageApi.MyOperation(key, ref input, ref output);
// 4. Write RESP response
if (status == GarnetStatus.OK)
{
ProcessOutput(output);
}
else
{
WriteError(CmdStrings.RESP_ERR_MY_MESSAGE);
}
return true;
}Key patterns:
parseState.GetArgSliceByRef(i) returns ref PinnedSpanByteStringInput/StringOutput (for string commands), ObjectInput/ObjectOutput (for object commands), or UnifiedInput/UnifiedOutput (for unified commands) before calling the storage APIProcessOutput(output) in the common case — this handles writing the RESP response from the output structRespServerSession extension methods (e.g., WriteError(...), WriteDirect(...), WriteInt64(...), etc.) — these handle SendAndReset() internallyu8 literals in CmdStrings (e.g., CmdStrings.RESP_ERR_MY_MESSAGE) rather than inlinetrue — there are no partial executionsObject commands (Hash, List, Set, SortedSet) follow a similar pattern to string commands. The main difference is that the RESP handler uses ObjectInput/ObjectOutput with the appropriate operation enum and must handle WRONGTYPE errors:
File: libs/server/Resp/Objects/[ObjectName]Commands.cs (e.g., SortedSetCommands.cs)
private unsafe bool SortedSetAdd<TGarnetApi>(ref TGarnetApi storageApi)
where TGarnetApi : IGarnetApi
{
if (parseState.Count < 3)
return AbortWithWrongNumberOfArguments("ZADD");
var key = parseState.GetArgSliceByRef(0);
var header = new RespInputHeader(GarnetObjectType.SortedSet) { SortedSetOp = SortedSetOperation.ZADD };
var input = new ObjectInput(header, ref parseState, startIdx: 1);
var output = GetObjectOutput();
var status = storageApi.SortedSetAdd(key, ref input, ref output);
switch (status)
{
case GarnetStatus.WRONGTYPE:
while (!RespWriteUtils.TryWriteError(CmdStrings.RESP_ERR_WRONG_TYPE, ref dcurr, dend))
SendAndReset();
break;
default:
ProcessOutput(output.SpanByteAndMemory);
break;
}
return true;
}Key differences from string commands:
ObjectInput with a RespInputHeader(GarnetObjectType.XXX) and an operation enum (e.g., SortedSetOperation.ZADD)GarnetStatus.WRONGTYPE — object commands can fail if the key holds a different object typelibs/server/Objects/[ObjectName]/[ObjectName]ObjectImpl.cs, dispatched via the operation enumFor new object sub-operations:
Add a value to the [ObjectName]Operation enum in libs/server/Objects/[ObjectName]/[ObjectName]Object.cs
and handle it in the Operate method's switch statement.
⚠️ Persisted, append-only: the object operation enums (
HashOperation,ListOperation,SetOperation,SortedSetOperation) have explicit values and are serialized to the AOF viaRespInputHeader.SubId. Append the new operation at the end with the next explicit value; never insert into the middle, reorder, or change an existing value.PersistedEnumStabilityTestsguards these values. The id occupies a full header byte, so up to 256 sub-operations per type are allowed.
Unified commands (EXISTS, DELETE, TYPE, TTL, EXPIRE, RENAME, etc.) are type-agnostic — they work on both raw string and object values. The RESP handler pattern is the same as string and object commands, but uses UnifiedInput/UnifiedOutput. The storage session layer uses the unified context (unifiedBasicContext), and the callbacks go in libs/server/Storage/Functions/UnifiedStore/.
Blocking commands (BLPOP, BRPOP, BLMOVE, BLMPOP, BZPOPMIN, BZPOPMAX, BZMPOP) follow a distinct pattern. They do not use ref storageApi and instead interact with the CollectionItemBroker (libs/server/Objects/ItemBroker/CollectionItemBroker.cs), which manages blocking/waiting behavior:
private unsafe bool SortedSetBlockingPop(RespCommand command)
{
if (parseState.Count < 2)
return AbortWithWrongNumberOfArguments(command.ToString());
if (!parseState.TryGetTimeout(parseState.Count - 1, out var timeout, out var error))
return AbortWithErrorMessage(error);
var keysBytes = new byte[parseState.Count - 1][];
for (var i = 0; i < keysBytes.Length; i++)
keysBytes[i] = parseState.GetArgSliceByRef(i).ToArray();
var result = storeWrapper.itemBroker.GetCollectionItemAsync(command, keysBytes, this, timeout).Result;
if (!result.Found)
{
WriteNull();
}
else
{
// Write RESP response with result.Key, result.Item, result.Score, etc.
}
return true;
}Key differences from regular commands:
RespServerSession.cs does NOT pass ref storageApi: RespCommand.BZMPOP => SortedSetBlockingMPop(),storeWrapper.itemBroker.GetCollectionItemAsync() which blocks (with timeout) until data is availableIGarnetApi method, no storage session method, and no RMW callbacks are needed for the blocking command itself (Steps 5-7 are skipped)CollectionItemBroker is notified when data is added to a collection (e.g., ZADD calls itemBroker.HandleCollectionUpdate(key)), which wakes up blocked clientsTryGetResult method in CollectionItemBroker.cs to map your RespCommand to the correct GarnetObjectType and implement the retrieval logicNote: Steps 5, 6, and 7 apply only to commands that read or write key-value data through the store (e.g.,
SET,GET,DELIFGREATER). Admin commands likeDEBUG,PING,CONFIG, etc. handle their logic entirely in the RESP handler (Step 4) and do not need API interface methods, storage session ops, or RMW callbacks. Skip to Step 8 for those. Blocking commands (e.g.,BZMPOP) also skip Steps 5-7 — see the blocking command pattern in Step 4.Note on context types: The unified single-store has three context types: string context (for raw string commands like GET/SET), object context (for collection commands like ZADD/LPUSH), and unified context (for type-agnostic commands like EXISTS/DELETE/TTL/EXPIRE). Most new commands use either the string or object context — the unified context is only for commands that must work across both value types.
File: libs/server/API/IGarnetApi.cs
Add method signature to IGarnetApi (read-write) or IGarnetReadApi (read-only):
// String command:
GarnetStatus MyOperation(PinnedSpanByte key, ref StringInput input, ref StringOutput output);
// Object command:
GarnetStatus MyOperation(PinnedSpanByte key, ref ObjectInput input, ref GarnetObjectStoreOutput output);
// Unified command:
GarnetStatus MyOperation(PinnedSpanByte key, ref UnifiedInput input, ref UnifiedOutput output);File: libs/server/API/GarnetApi*.cs
Add delegation in the GarnetApi partial struct. The implementation goes in the appropriate partial file based on the context type:
GarnetApi.cs — string commandsGarnetApiObjectCommands.cs — object commandsGarnetApiUnifiedCommands.cs — unified commandspublic GarnetStatus MyOperation(PinnedSpanByte key, ref StringInput input, ref StringOutput output)
=> storageSession.MyOperation(key, ref input, ref output);⚠️ Caveat: GarnetApi is a generic partial struct: GarnetApi<TStringContext, TObjectContext, TUnifiedContext>. Always add your method to the correct partial file for the context type you're using.
Overloads for programmatic callers: In addition to the primary signature (used by the network handler), you can add simpler overloads for programmatic/embedded callers that avoid forcing them to create the Input/Output structs. For example:
public GarnetStatus MyOperation(PinnedSpanByte key, double val, out double output)This overload internally creates the appropriate input/output structs and only returns the desired value to the caller, instead of writing to the output buffer.
File: New or existing file in libs/server/Storage/Session/MainStore/ (for string-context ops), libs/server/Storage/Session/ObjectStore/ (for object-context ops), or libs/server/Storage/Session/UnifiedStore/ (for unified-context ops)
This layer wraps Tsavorite API calls. The network path uses a generic context parameter:
public GarnetStatus MyOperation<TStringContext>(PinnedSpanByte key, ref StringInput input, ref StringOutput output, ref TStringContext context)
where TStringContext : ITsavoriteContext<...>
{
var status = context.RMW((FixedSpanByteKey)key, ref input, ref output);
if (status.IsPending)
CompletePendingForSession(ref status, ref output, ref context);
return GarnetStatus.OK;
}Object and unified commands follow the same pattern — just substitute the appropriate context, input, and output types:
| Context type | Input type | Output type | Helper method |
|---|---|---|---|
| String | StringInput | StringOutput | context.RMW(...) / context.Read(...) |
| Object | ObjectInput | GarnetObjectStoreOutput | RMWObjectStoreOperation(...) / ReadObjectStoreOperation(...) |
| Unified | UnifiedInput | UnifiedOutput | context.RMW(...) / context.Read(...) |
Programmatic overloads: You can also add simpler overloads for programmatic callers (see Step 5 note). These internally create the input/output structs and return only the desired value.
Object commands use the same pattern as above with ObjectInput/GarnetObjectStoreOutput and the object context.
⚠️ Caveat — HandleCollectionUpdate: If your object command modifies a collection (adds/removes elements), call itemBroker.HandleCollectionUpdate(key) after the store operation. This wakes up any clients blocked on that key (e.g., via BZPOPMIN). The actual data logic is implemented in the object class, not in the storage session.
File: libs/server/Objects/[ObjectName]/[ObjectName]ObjectImpl.cs
For object commands, the core logic lives in the object implementation. The Operate method in [ObjectName]Object.cs dispatches to implementation methods based on the operation enum:
case SortedSetOperation.ZADD:
SortedSetAdd(ref input, ref output.SpanByteAndMemory);
break;The implementation methods in [ObjectName]ObjectImpl.cs directly manipulate the object's internal data structures (e.g., sortedSet, sortedSetDict for SortedSet).
File: libs/server/Storage/Functions/MainStore/RMWMethods.cs (string commands) or libs/server/Storage/Functions/UnifiedStore/RMWMethods.cs (unified commands)
If your command uses RMW, you must handle these callbacks:
| Callback | When | Purpose |
|---|---|---|
NeedInitialUpdate | Key doesn't exist | Return true to create record |
InitialUpdater | Creating new record | Write initial value |
NeedCopyUpdate | Key exists, record needs copy | Return true to copy-update, false to skip |
InPlaceUpdater | Key exists, update in place | Modify existing value |
CopyUpdater | Key exists, copy to new record | Write updated value to new record |
Add a case RespCommand.MYCMD: to each relevant switch statement.
For Read commands (MainStore): If your command uses Read (not RMW), the read response logic lives in libs/server/Storage/Functions/MainStore/PrivateMethods.cs — add a case to the CopyRespToWithInput method.
File: libs/server/Storage/Functions/MainStore/VarLenInputMethods.cs (string commands) or libs/server/Storage/Functions/UnifiedStore/VarLenInputMethods.cs (unified commands)
If your command writes a value, you must specify the value length:
| Method | Purpose |
|---|---|
GetRMWInitialFieldInfo | Size of value for new records |
GetRMWModifiedFieldInfo | Size of value for updated records |
⚠️ Caveat — RecordType:
If your command creates records with a custom RecordType (e.g., for type discrimination), you can set it in InitialUpdater after record initialization:
var header = logRecord.RecordDataHeader;
header.RecordType = MyManager.MyRecordType;This works because RecordDataHeader.RecordType has a setter that writes through a raw pointer. No Tsavorite infrastructure changes needed (despite the TODO comments in LogRecord.cs).
File: playground/CommandInfoUpdater/SupportedCommand.cs
Add entry following the existing ordering/grouping in the file:
new("MY.CMD", RespCommand.MYCMD, StoreType.Main),Note: The file is not strictly alphabetical — entries are grouped by category (e.g., script commands at the end). Follow the existing grouping conventions rather than inserting strictly alphabetically.
For admin/non-key commands (e.g., DEBUG, PING), omit StoreType or use StoreType.None:
new("DEBUG", RespCommand.DEBUG),StoreType values: Main (string store), Object (object store), All (both), None (no keys).
File: playground/CommandInfoUpdater/GarnetCommandsInfo.json
Needed for commands that don't exist in standard Redis (e.g., DELIFGREATER, SETIFMATCH), or standard Redis commands whose info you need to override. Standard Redis commands (e.g., DEBUG, GETDEL) normally get their metadata from a running RESP server automatically via the CommandInfoUpdater tool — skip this step and Step 8c for those unless you need to override their info.
Overriding a command that also exists on the baseline server (e.g.,
OBJECT): the tool merges per sub-command, by name — yourGarnetCommandsInfo.json/GarnetCommandsDocs.jsonentry wins and the baseline server fills in any sub-commands you did not specify. The override replaces the command's base fields wholesale (it is not a field-by-field merge), so include the parent command plus the sub-commands you want to control. This is howOBJECTreports Garnet-accurate sub-command summaries while the tool still generates the rest.
Add a JSON entry:
{
"Command": "MYCMD",
"Name": "MY.CMD",
"IsInternal": false,
"Arity": -2,
"Flags": "DenyOom, Write",
"FirstKey": 1,
"LastKey": 1,
"Step": 1,
"AclCategories": "Slow, Write, Garnet",
"KeySpecifications": [
{
"BeginSearch": { "TypeDiscriminator": "BeginSearchIndex", "Index": 1 },
"FindKeys": { "TypeDiscriminator": "FindKeysRange", "LastKey": 0, "KeyStep": 1, "Limit": 0 },
"Flags": "RW, Insert"
}
],
"StoreType": "Main"
}Key fields:
Arity: Positive = exact arg count (including command name); Negative = minimumFlags: ReadOnly, Write, DenyOom, Fast, Admin, NoAuth, Module, etc.AclCategories: Used for ACL permission checks. Use Garnet for Garnet-specific commandsKeySpecifications: Drives automatic transaction key locking — no per-command switch neededKeySpec.Flags: RO (read-only), RW (read-write), Access, Insert, Update, DeleteFile: playground/CommandInfoUpdater/GarnetCommandsDocs.json
Note: This step is not necessary for internal commands. The main purpose of command docs is to enable client auto-complete for the command.
Add documentation entry:
{
"Command": "MYCMD",
"Name": "MY.CMD",
"Summary": "Description of what the command does.",
"Group": "Generic",
"Complexity": "O(1)",
"Arguments": [
{
"TypeDiscriminator": "RespCommandKeyArgument",
"Name": "KEY",
"DisplayText": "key",
"Type": "Key",
"KeySpecIndex": 0
}
]
}⚠️ Caveat — Group must be a valid RespCommandGroup enum value:
Bitmap, Cluster, Connection, Generic, Geo, Hash, HyperLogLog, List, Module, PubSub, Scripting, Sentinel, Server, Set, SortedSet, Stream, String, Transactions
Do NOT invent new group names — the JSON deserializer will fail.
⚠️ CRITICAL: Never edit libs/resources/RespCommandsInfo.json or libs/resources/RespCommandsDocs.json directly. These are generated by the CommandInfoUpdater tool.
Steps:
valkey-server --port 6399cd playground/CommandInfoUpdater
dotnet build -f net10.0
dotnet run -f net10.0 --no-build -- --port 6399 --output ../../libs/resources--port must match the port of the local RESP server.)Would you like to continue? (Y/N) twice (once for info, once for docs). Press Y for both, or pass --yes (see below) to auto-confirm.⚠️ Caveat: By default the tool prompts via Console.ReadKey(), which does NOT work with piped input (echo "Y" | dotnet run ... fails). For non-interactive / scripted runs (including AI agents), pass --yes to auto-confirm both prompts; otherwise run it in a real interactive terminal.
⚠️ Caveat: The tool requires a running RESP-compatible server (e.g., Valkey or Redis — not Garnet) to query standard command metadata. For Garnet-only commands, the tool reads from GarnetCommandsInfo.json and GarnetCommandsDocs.json instead.
⚠️ Caveat — unexpected "commands to remove": If the tool reports commands to remove that you did not intend to delete, those commands exist in the resource files but are missing from SupportedCommand.cs. Register them in SupportedCommand.cs (and, for Garnet-only commands, in the override JSONs) rather than reaching for the --ignore flag. Every command Garnet ships should be registered, so a normal run needs no --ignore.
See also:
playground/CommandInfoUpdater/README.mddocuments the tool in full — the inputs you edit, the per-sub-command override/merge behavior, the baseline server (Docker), the--ignoreescape hatch, and how to surgically regenerate a single command.
File: test/Garnet.test/Resp/ACL/RespCommandTests.cs
⚠️ CRITICAL: The AllCommandsCovered test automatically validates that every RespCommand enum value has a corresponding ACL test. If you add a command without an ACL test, AllCommandsCovered will fail.
Test naming convention:
ACLs or ACLsAsyncRI.CREATE → RICreateACLsAsyncPattern (follow SADD for non-idempotent commands):
[Test]
public async Task MyCommandACLsAsync()
{
int count = 0;
await CheckCommandsAsync(
"MY.CMD",
[DoMyCommandAsync]
).ConfigureAwait(false);
async Task DoMyCommandAsync(GarnetClient client)
{
var val = await client.ExecuteForStringResultAsync("MY.CMD",
[$"key-{count}", "arg1"]).ConfigureAwait(false);
count++;
ClassicAssert.AreEqual("OK", val);
}
}⚠️ Caveat — Idempotency: The ACL framework calls your command multiple times (for different user/permission combinations). If your command is NOT idempotent (e.g., RI.CREATE fails on duplicate), use a counter to generate unique keys per invocation (see the SADD ACL test pattern).
File: Add tests to an existing or new file in test/Garnet.test/:
test/Garnet.test/RespTests.cstest/Garnet.test/Resp[ObjectName]Tests.cs (e.g., RespSortedSetTests.cs)test/Garnet.test/Resp<Feature>Tests.cs if the command doesn't fit existing test filesRequired structure:
[TestFixture]
public class RespMyFeatureTests : TestBase
{
GarnetServer server;
[SetUp]
public void Setup()
{
TestUtils.DeleteDirectory(TestUtils.MethodTestDir, wait: true);
server = TestUtils.CreateGarnetServer(TestUtils.MethodTestDir);
server.Start();
}
[TearDown]
public void TearDown()
{
server.Dispose();
TestUtils.OnTearDown();
}
[Test]
public void MyBasicTest()
{
using var redis = ConnectionMultiplexer.Connect(TestUtils.GetConfig());
var db = redis.GetDatabase(0);
var result = db.Execute("MY.CMD", "key", "value");
ClassicAssert.AreEqual("OK", (string)result);
}
}Note: Test fixtures must inherit from TestBase.
Recommended test cases:
File: website/docs/commands/ — choose the appropriate markdown file based on the command category (e.g., garnet-specific.md for Garnet-only commands, api-compatibility.md to mark a standard Redis command as supported).
Add a section documenting the command syntax, description, and response format:
### **MY.CMD**
#### **Syntax**
```bash
MY.CMD key valueDescription of what the command does.
Also mark the command as supported in `website/docs/commands/api-compatibility.md` if it corresponds to a standard Redis command.
---
## Step 11b: Add Configuration Settings (if needed)
If the command is optional, gated behind a feature flag, or needs a server-side configuration parameter (e.g., `DEBUG` requires `--enable-debug-command`), you must wire up a configuration option across four files:
### 1. Add property to `Options` class
**File:** `libs/host/Configuration/Options.cs`
Add a property with the `[Option]` attribute (from CommandLineParser):
```csharp
[OptionValidation]
[Option("enable-my-feature", Required = false, HelpText = "Enable MY.CMD for 'no', 'local' or 'all' connections")]
public ConnectionProtectionOption EnableMyFeature { get; set; }The [Option] attribute defines the CLI flag name (kebab-case). Use Required = false for optional settings. Common types: bool, int, string, ConnectionProtectionOption (for no/local/yes connection gating), or custom enums.
GarnetServerOptionsFile: libs/server/Servers/GarnetServerOptions.cs
Add a matching field:
/// <summary>
/// Enables MY.CMD
/// </summary>
public ConnectionProtectionOption EnableMyFeature;Then in Options.GetServerOptions() (in Options.cs), map the property:
EnableMyFeature = EnableMyFeature,File: libs/host/defaults.conf
Add the default in the appropriate section:
/* Enable MY.CMD for clients - no/local/yes */
"EnableMyFeature": "no",Access the setting via storeWrapper.serverOptions:
if (storeWrapper.serverOptions.EnableMyFeature == ConnectionProtectionOption.No)
{
while (!RespWriteUtils.TryWriteError("ERR command not enabled"u8, ref dcurr, dend))
SendAndReset();
return true;
}File: test/Garnet.test/GarnetServerConfigTests.cs
Test that the setting is parsed correctly from CLI args and config files.
dotnet build Garnet.slnx -c Debugdotnet format Garnet.slnx --verify-no-changes⚠️ Caveat: New files commonly fail with FINALNEWLINE errors. Ensure files do NOT have a trailing newline at the very end. Fix with: perl -pi -e 'chomp if eof' path/to/file.cs
dotnet test test/standalone/Garnet.test -f net10.0 -c Debug --filter "FullyQualifiedName~RespMyFeatureTests"dotnet test test/standalone/Garnet.test -f net10.0 -c Debug --filter "FullyQualifiedName~AllCommandsCovered"dotnet test test/standalone/Garnet.test -f net10.0 -c Debug --filter "FullyQualifiedName~RespTests"Test ports are claimed automatically per test host, so working copies sharing a machine do not collide and
nothing needs to be set; see .github/copilot-instructions.md. Check the output for error CS before trusting a pass — with --no-build, or
when a compile error surfaces in a project that is not rebuilt, the previously built assembly runs instead.
For standard commands, transaction key locking is automatic — driven by KeySpecifications in the command metadata JSON. No per-command code is needed in TxnKeyManager.cs.
For custom multi-key operations that don't fit the standard key spec pattern, manually call txnManager.SaveKeyEntryToLock(key, lockType).
Dot-prefixed commands (e.g., RI.CREATE): The RESP wire name uses a dot, but the enum name cannot. The ACL parser, AllCommandsCovered test, and SupportedCommand.cs all need to handle the dot-to-enum mapping. Check ACLParser.cs for normalization logic.
AllCommandsCovered is strict: It reflects over ALL RespCommand enum values and ALL entries in RespCommandsInfo.json. Missing either an ACL test or a JSON entry will fail this test.
NeedCopyUpdate for create-only commands: If your command should NOT overwrite existing records (like SETNX), return false from NeedCopyUpdate for your command. Otherwise Tsavorite will attempt a copy-update when the record can't be updated in-place.
VarLenInputMethods.cs is easy to forget: If your command creates or modifies records via RMW, you must add cases to GetRMWInitialFieldInfo and GetRMWModifiedFieldInfo. Without this, Tsavorite won't allocate the right amount of space for your value.
Resource JSON files are generated, not hand-edited: libs/resources/RespCommandsInfo.json and libs/resources/RespCommandsDocs.json are generated by playground/CommandInfoUpdater. Edit the source files (GarnetCommandsInfo.json, GarnetCommandsDocs.json, SupportedCommand.cs) and run the tool.
RespCommandGroup enum is closed: The Group field in docs JSON must match a value in the RespCommandGroup enum (libs/server/Resp/RespCommandDocs.cs). Use Generic if no existing group fits.
File headers: All C# files require // Copyright (c) Microsoft Corporation. / // Licensed under the MIT license.
Test resource usage: Use small values for buffer sizes, cache sizes, etc. in tests. Don't allocate 16MB+ buffers when 64KB will do.
The RI.CREATE command was the first command implemented following this guide. Key files for reference:
| File | Purpose |
|---|---|
libs/server/Resp/RangeIndex/RespServerSessionRangeIndex.cs | RESP handler with option parsing |
libs/server/Resp/RangeIndex/RangeIndexManager.cs | Manager pattern for external data |
libs/server/Resp/RangeIndex/RangeIndexManager.Index.cs | Fixed-size stub struct in store |
libs/server/Storage/Session/MainStore/RangeIndexOps.cs | StorageSession → RMW flow |
test/Garnet.test/RespRangeIndexTests.cs | Integration tests |
test/Garnet.test/Resp/ACL/RespCommandTests.cs | ACL test (search for RICreateACLsAsync) |
© microsoft, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .github/skills/add-garnet-command of microsoft/garnet.
Open the folder on GitHubat commit 653bb32
Add Garnet RESP Command next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Add Garnet RESP Command this skillmicrosoft/garnet | 12k | — | ~9.7k | Automated safety check: Pass | MIT | |
| Extend Commands APIredis/lettuce | 5.8k | — | ~7.7k | Automated safety check: Notes | MIT | |
| Gumroad Prod Consoleantiwork/gumroad | 9.8k | — | ~2.9k | Automated safety check: Notes | MIT | |
| Configuring Horizoncoollabsio/coolify | 63k | 4 repos | ~898 | Automated safety check: Pass | MIT | |
| Go Redis Client Test Runnerredis/go-redis | 22k | — | ~786 | Automated safety check: Pass | BSD-2-Clause | |
| FastAPI-Redis SDK Developmentredis/fastapi-redis-sdk | 404 | — | ~2.5k | Automated safety check: Notes | MIT |
redis/lettuce
Add or extend Redis commands in the Lettuce client API end-to-end — a new core command, a family of new commands, an extension to an existing command's options, or a module/area command…
antiwork/gumroad
Execute read-only Ruby/Rails commands against Gumroad's production database for debugging and investigation.
coollabsio/coolify
A skill your agent uses whenever the user mentions Horizon by name in a Laravel context.
redis/go-redis
Explains how to run go-redis tests: the Docker Compose stack, make targets, focusing a single Ginkgo spec, the e2e suite and the version environment variables.
redis/fastapi-redis-sdk
Guides development on the fastapi-redis-sdk library itself - its connection lifecycle, dependency-injected caching, and async/sync bridging.
redis/jedis
Add or extend Redis commands in the Jedis client API — a new core command, a family of new commands, an extension to an existing command's options, or a module command (Search/TimeSeries/JSON/Bloom).
microsoft/garnet
Checks that a pull request's title and description match its implementation and reviews the code for Garnet best practices, reporting findings without posting them.
Works with
Categories
Step-by-step guide for adding a new built-in RESP command to Garnet, from the command enum and parser to storage callbacks, command metadata JSON and tests. cs, a RESP handler file, the IGarnetApi interface and its delegation, the storage session, RMW and read callbacks, variable-length methods, object operations, and an item broker for blocking commands. A table marks which areas are always required and which apply only to key-value, string, object or blocking commands.
Add Garnet RESP Command fits situations like: adding a new server command such as RI.SET to Garnet; working out which Garnet files a new RESP command has to touch; adding ACL and integration tests for a new built-in command; regenerating the command metadata JSON after adding a command.
Run `npx skills add microsoft/garnet --skill add-garnet-command -a claude-code`. Or copy the skill folder (.github/skills/add-garnet-command in microsoft/garnet) into .claude/skills/add-garnet-command in your project. Claude Code loads it when a task matches its description.
Run `npx skills add microsoft/garnet --skill add-garnet-command -a codex`. Or copy the skill folder (.github/skills/add-garnet-command in microsoft/garnet) into .agents/skills/add-garnet-command in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add microsoft/garnet --skill add-garnet-command -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/add-garnet-command, .gemini/skills/add-garnet-command, .github/skills/add-garnet-command and .opencode/skills/add-garnet-command in your project.
Going by SKILL.md and its folder, Add Garnet RESP Command needs the command-line tools its instructions call (dotnet). Our summary lists: A checkout of the Garnet repository.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Add Garnet RESP Command is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.7k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Add Garnet RESP Command: Extend Commands API (redis/lettuce, 5.8k stars), Gumroad Prod Console (antiwork/gumroad, 9.8k stars), Configuring Horizon (coollabsio/coolify, 63k stars) and Go Redis Client Test Runner (redis/go-redis, 22k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
microsoft (a GitHub organization, an official publisher) maintains it in microsoft/garnet, which has 12,042 GitHub stars. The repository holds 2 skills in this directory. The repository was last updated on October 7, 2026.
Source: microsoft/garnet on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.