grpcurl-cli
Wraps grpcurl, the curl-equivalent CLI for gRPC. Covers descriptor sources (server reflection default, --import-path + --proto for proto files, --protoset for compiled descriptor sets), service discovery (`list`, `describe`), invoking unary RPCs (`-d '{...}'`, `-d @file.json`, `-d @` for stdin), streaming RPCs (newline-delimited JSON via stdin), TLS configuration (--cacert, --cert, --key, --insecure, --plaintext), header injection (-H 'Authorization: Bearer ...'), and exit codes. Use for ad-hoc gRPC debugging, smoke testing, scriptable PR-time gates, and CLI-based interaction with reflective gRPC services. Composes grpc-status-code-mapping-reference for interpreting non-OK responses.
grpcurl-cli
Overview
Per github.com/fullstorydev/grpcurl, grpcurl can invoke any gRPC method using one of three descriptor sources (server reflection, .proto source files, or compiled .protoset files) and emits JSON responses. This skill wraps grpcurl for: ad-hoc debugging, scriptable smoke tests, and CI-based contract verification.
When to use
Authoring
Install
Per grpcurl README:
# Homebrew
brew install grpcurl
# Go install
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
# Docker
docker pull fullstorydev/grpcurl:latestVerify:
grpcurl -versionChoose a descriptor source
| Source | When |
|---|---|
| Server reflection (default) | Server implements the grpc.reflection.v1 reflection service |
Proto source files (-import-path + -proto) | Reflection disabled; you have the .proto files |
Protoset file (-protoset) | Pre-compiled descriptor distributed alongside the binary |
Server reflection is the easiest but security-sensitive - many production deployments disable it (reflection.Register(s) not called). Plan accordingly.
Generate a protoset if neither reflection nor proto files are available at call time:
protoc --proto_path=. \
--descriptor_set_out=myservice.protoset \
--include_imports \
my/custom/server/service.protoRunning
Discover
Per grpcurl README:
# List services
grpcurl -plaintext localhost:8787 list
# List methods in a service
grpcurl -plaintext localhost:8787 list my.custom.server.Service
# Describe a method (full schema)
grpcurl -plaintext localhost:8787 describe my.custom.server.Service.MethodOne
# Export proto schemas
grpcurl -plaintext -proto-out-dir "out_protos" localhost:8787 \
describe my.custom.server.Service
# Export a protoset
grpcurl -plaintext -protoset-out "out.protoset" localhost:8787 \
describe my.custom.server.ServiceInvoke unary RPCs
# Plaintext (no TLS)
grpcurl -plaintext \
-d '{"id": 1234, "tags": ["foo","bar"]}' \
localhost:8080 my.custom.server.Service/Method
# TLS with server-only cert
grpcurl -cacert ca.crt \
-d '{"id": 1234}' \
grpc.example.com:443 my.custom.server.Service/Method
# Mutual TLS
grpcurl -cacert ca.crt -cert client.crt -key client.key \
-d '{"id": 1234}' \
grpc.example.com:443 my.custom.server.Service/Method| Flag | Use |
|---|---|
-plaintext | No TLS (dev / internal) |
-insecure | TLS, but skip verification |
-cacert | CA certificate file |
-cert, -key | Client certificate + private key (mTLS) |
-H | Add header (repeat for multiple) |
-d | Inline JSON request payload |
-d @<file> | Read JSON from file |
-d @ | Read JSON from stdin |
-import-path | Proto import path |
-proto | Proto file path |
-protoset | Compiled descriptor set |
Auth headers
grpcurl -H "Authorization: Bearer ${TOKEN}" \
-H "x-request-id: $(uuidgen)" \
-d '{"id": 1234}' \
grpc.example.com:443 my.custom.server.Service/MethodStreaming RPCs
Per grpcurl README: for bidi or client-streaming, use newline-delimited JSON via -d @:
grpcurl -plaintext \
-d @ \
localhost:8080 my.custom.server.Service/StreamMethod << EOM
{"request": 1}
{"request": 2}
{"request": 3}
EOMEach line is one message to the server. Closing stdin signals end-of-stream.
Reproducing a production bug
# Capture failing request from logs / tracing
PAYLOAD=$(cat trace-payload.json)
# Reproduce against a staging instance
grpcurl -cacert ca.crt \
-H "Authorization: Bearer ${TOKEN}" \
-d "${PAYLOAD}" \
staging.example.com:443 my.custom.server.Service/Method
# Compare status code per grpc-status-code-mapping-referenceParsing results
Successful response
{
"id": "1234",
"name": "alice",
"createdAt": "2026-05-01T10:30:00Z"
}Exit code 0 → RPC succeeded (gRPC OK).
Error response
ERROR:
Code: NotFound
Message: User 1234 does not existPer grpc-status-code-mapping-reference, NotFound (code 5) maps to HTTP 404 and indicates the resource isn't present. Exit code is non-zero.
Exit codes
| Exit code | Meaning |
|---|---|
| 0 | RPC succeeded |
| non-zero | RPC failed (Code printed to stderr) |
For scripting:
if grpcurl -plaintext -d '{"id":"x"}' \
localhost:8080 my.Service/GetUser > /tmp/out 2>&1; then
echo "RPC ok"
else
code=$(grep "Code:" /tmp/out | awk '{print $2}')
echo "RPC failed: $code"
fiFor richer parsing in CI: -format=json and use jq:
grpcurl -plaintext -format=json -d '{"id":"x"}' \
localhost:8080 my.Service/GetUser \
| jq '.id'CI integration
Smoke test on deploy
# .github/workflows/grpc-smoke.yml
name: grpc-smoke
on:
deployment_status:
jobs:
smoke:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- name: Install grpcurl
run: |
curl -L https://github.com/fullstorydev/grpcurl/releases/latest/download/grpcurl_linux_x86_64.tar.gz | tar xz
sudo mv grpcurl /usr/local/bin/
- name: Verify health endpoint
run: |
grpcurl -cacert ./certs/ca.crt \
${{ github.event.deployment.payload.host }}:443 \
grpc.health.v1.Health/Check
- name: Sample request
env:
TOKEN: ${{ secrets.SMOKE_TEST_TOKEN }}
run: |
grpcurl -cacert ./certs/ca.crt \
-H "Authorization: Bearer ${TOKEN}" \
-d '{"id":"smoke-test-1"}' \
${{ github.event.deployment.payload.host }}:443 \
user.v1.UserService/GetUserExit code 0 → deploy is verified responsive. Non-zero → alert.
Reflection-based smoke matrix
Where reflection is enabled, smoke-test every RPC:
SERVICES=$(grpcurl -plaintext localhost:8080 list | grep -v '^grpc')
for service in $SERVICES; do
METHODS=$(grpcurl -plaintext localhost:8080 list "$service")
for method in $METHODS; do
# Skip unary methods that require payload
grpcurl -plaintext "localhost:8080" "$method" 2>&1 \
| grep -q "Code: \(Unimplemented\|InvalidArgument\)" || \
echo "WARN: $method may be unreachable"
done
doneThis finds methods registered in reflection but returning Unimplemented (per grpc-status-code-mapping-reference).
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
-insecure against production endpoints | MITM exposure; certificate validation bypass | Always provide -cacert |
| Reflection enabled in production | Schema disclosure; attack surface | Disable reflection in prod; use protoset distribution |
| Long-lived bearer tokens in CI scripts | Token leak via logs / artifacts | Short-lived tokens; mask in CI logs |
| Comparing stdout text across versions | grpcurl reformats; test brittleness | Compare JSON via -format=json + jq |
| Smoke-testing a write RPC | Side effects in prod / staging | Health-check + read-only smoke only |
Stream RPC with -d inline (no @) | Only one message sent | Use -d @ + stdin |
No -H "x-request-id" | Hard to trace from server logs | Always inject a unique ID for traceability |
--proto without --import-path for multi-file schemas | Import resolution fails | Set --import-path to project root |