Skip to content

QHx CLI Reference

The qhx CLI is a compartment-aware wrapper around kubectl that automatically applies MLS labels and enforces compartment boundaries. It provides the same interface as kubectl while adding security context awareness.

Key features:

  • Automatic MLS label application
  • Compartment context switching
  • Cross-compartment access prevention
  • Enhanced output with compartment information
  • Compatible with kubectl commands

Target audience: Developers, operators, and security teams working with MLS-classified workloads.

Terminal window
# Linux (amd64)
curl -LO https://github.com/messier42/qhx-cli/releases/latest/download/qhx-linux-amd64
chmod +x qhx-linux-amd64
sudo mv qhx-linux-amd64 /usr/local/bin/qhx
# macOS (amd64)
curl -LO https://github.com/messier42/qhx-cli/releases/latest/download/qhx-darwin-amd64
chmod +x qhx-darwin-amd64
sudo mv qhx-darwin-amd64 /usr/local/bin/qhx
# macOS (arm64)
curl -LO https://github.com/messier42/qhx-cli/releases/latest/download/qhx-darwin-arm64
chmod +x qhx-darwin-arm64
sudo mv qhx-darwin-arm64 /usr/local/bin/qhx
# Verify installation
qhx version
Terminal window
# Requires Go 1.21+
git clone https://github.com/messier42/qhx-cli.git
cd qhx-cli
make install
Terminal window
# Bash
qhx completion bash | sudo tee /etc/bash_completion.d/qhx
# Zsh
qhx completion zsh | sudo tee /usr/share/zsh/site-functions/_qhx
# Fish
qhx completion fish | sudo tee ~/.config/fish/completions/qhx.fish

The CLI reads configuration from ~/.qhx/config.yaml:

~/.qhx/config.yaml
currentContext: us-ts-gestalt
contexts:
us-ts-gestalt:
country: US
sensitivity: TS
compartment: GESTALT
kubeContext: production-cluster
namespace: default
uk-s-marble:
country: UK
sensitivity: S
compartment: MARBLE
kubeContext: production-cluster
namespace: default
au-c-baseline:
country: AU
sensitivity: C
compartment: BASELINE
kubeContext: staging-cluster
namespace: default
# Default user groups (applied to all contexts)
defaultUserGroups:
- system:authenticated
# Kubeconfig path (optional, defaults to ~/.kube/config)
kubeconfig: ~/.kube/config
Terminal window
# Set compartment via environment
export QHX_COUNTRY=US
export QHX_SENSITIVITY=TS
export QHX_COMPARTMENT=GESTALT
# Override kubeconfig
export KUBECONFIG=~/.kube/production-config
# Debug mode
export QHX_DEBUG=true

Precedence: Environment variables override configuration file values.

Make HTTP requests with workload identity validation and request notarization.

Purpose: Test QHx-protected services and verify notarization levels.

Usage:

Terminal window
qhx curl [flags] <url>

Notarization Levels:

The -n or --notarization-level flag selects the notarization level:

  • workload (default) - Validate workload identity statement only (lowest overhead)
  • logRequest - Validate workload identity; request is logged by notary
  • signRequest - Validate workload identity and signed receipt; highest notarization (highest overhead)

Key Features:

  • Validates QHx workload identity statements
  • Verifies request notarization receipts
  • Prints detailed notarization data
  • Compatible with standard curl flags
  • Exits with non-zero code if validation fails

Examples:

Terminal window
# Basic request with default (workload) notarization
qhx curl http://api-server:8080/health
# Request with signed receipt
qhx curl -n signRequest http://api-server:8080/api/inference
# POST request with data
qhx curl -X POST -d '{"query":"hello"}' \
-H "Content-Type: application/json" \
http://api-server:8080/api/chat
# Print workload identity statement
qhx curl --print-workload http://api-server:8080/
# Print request receipt (requires signRequest)
qhx curl -n signRequest --print-receipt http://api-server:8080/
# Show response headers
qhx curl -i http://api-server:8080/
# Silent mode (no progress)
qhx curl -s http://api-server:8080/health

Workload Identity Statement Output:

Terminal window
qhx curl --print-workload http://ollama-server:8080/
# Output:
Trust Domain: qhx.dev
SPIFFE ID: spiffe://qhx.dev/ns/ai-demo/sa/ollama/pod/ollama-abc123/...
Namespace: ai-demo
Service Account: ollama
Pod Name: ollama-server-786f9f59d-5djst
Pod UID: 9d86dd35-31fd-4271-bb10-579bfe434d58
Container Images:
- ollama/ollama:latest
- qhx/proxy:v0.6.1
Labels:
app: ollama-server
pod-template-hash: 786f9f59d
Annotations:
mls.qhx.dev/compartment: GESTALT
mls.qhx.dev/level: TS
Raw Claims (JSON):
{
"iss": "spiffe://qhx.dev/ns/qhx-central/sa/central-proxy/...",
"sub": "spiffe://qhx.dev/ns/ai-demo/sa/ollama/...",
"aud": "https://qhx.dev/workload-statement",
"iat": 1760673752,
"qhx": {
"trustDomain": "qhx.dev",
"namespace": "ai-demo",
"podName": "ollama-server-786f9f59d-5djst",
...
}
}

Request Receipt Output:

Terminal window
qhx curl -n signRequest --print-receipt \
-X POST -d '{"model":"llama3.2:1b","messages":[{"role":"user","content":"Hello"}]}' \
http://ollama-server:8080/v1/chat/completions
# Output:
Signed Request Receipt "K8MKUqJsgmenKSchf06c5w==":
==================================================
Issuer: spiffe://qhx.dev/ns/qhx-central/sa/central-proxy/...
Audience: [https://qhx.dev/receipt]
Issued At: 2026-02-02T10:48:07Z
Workload Statement ID: VGkHGqleoIQHBiGlowdMh947YojeuMwTjhtELksaPs8=
HTTP Request:
Method: POST
Path: /v1/chat/completions
Headers:
Content-Type: application/json
Body (110 bytes): {"model":"llama3.2:1b",...}
HTTP Response:
Status Code: 200
Headers:
Content-Type: application/json
Body (351 bytes): {"id":"chatcmpl-500",...}
Raw Claims (JSON):
{
"iss": "spiffe://qhx.dev/ns/qhx-central/sa/central-proxy/...",
"aud": "https://qhx.dev/receipt",
"iat": 1760680087,
"qhx": {
"workloadStatementID": "VGkHGqleoIQHBiGlowdMh947...",
"request": {...},
"response": {...}
}
}

Flags:

FlagDescription
-d, --data <str>Specify HTTP POST data
-H, --header <str>Add HTTP header to request
-n, --notarization-level <level>Select notarization level (workload, logRequest, signRequest)
--print-workloadPrint workload identity statement
--print-receiptPrint request notarization receipt (requires signRequest)
-X, --request <method>Select HTTP request method (GET, POST, PUT, DELETE, etc.)
-i, --show-headersShow HTTP headers in response
-s, --silentSilent mode (no progress output)
--validate <bool>Require workload statement verification (default: true)

Global Flags:

FlagDescription
--log-level <level>Enable development logging (console output, debug level)
--log-format <format>Log output format: text or json (default: text)
--log-stacktraceInclude stack traces in error logs
--spiffe-socket-path <path>Path to Workload API socket (default: unix:///spiffe-workload-api/agent.sock)
-v, --verbose <int>Log verbosity level (higher is more verbose, default: -2)

Use Cases:

1. Testing AI Inference with Audit Trail:

Terminal window
# Call LLM with signed receipt for compliance
qhx curl -n signRequest --print-receipt \
-X POST -H "Content-Type: application/json" \
-d '{"model":"llama3.2:1b","messages":[{"role":"user","content":"Classify this document"}]}' \
http://ollama-server:8080/v1/chat/completions \
> inference-receipt.json
# Receipt proves:
# - Which model handled the request
# - Exact request/response
# - Timestamp
# - Workload identity

2. Verifying Service Identity:

Terminal window
# Confirm which workload is running
qhx curl --print-workload http://api-server:8080/health | grep "Container Images"
# Output shows exact OCI image digest:
# Container Images:
# - api-server:v1.0@sha256:abc123...

3. Debugging Network Policies:

Terminal window
# Test if request is allowed
qhx curl http://blocked-service:8080/
# If blocked by policy:
# Error: Connection refused (likely NetworkPolicy denial)
# If blocked by QHx:
# Error: Workload statement verification failed

4. Performance Testing:

Terminal window
# Test overhead of different notarization levels
time qhx curl -n workload http://api:8080/ # ~1ms overhead
time qhx curl -n logRequest http://api:8080/ # ~2ms overhead
time qhx curl -n signRequest http://api:8080/ # ~5-10ms overhead

Exit Codes:

  • 0 - Success (request completed and notarization validated)
  • 1 - Network error (connection failed)
  • 2 - Workload statement validation failed
  • 3 - Request receipt validation failed
  • 4 - HTTP error (4xx or 5xx response)

Validation Behavior:

By default, qhx curl requires workload statement validation (--validate=true). If the server does not return valid notarization headers, the command fails:

Terminal window
# Calling non-QHx service fails validation
qhx curl http://external-api.com/
# Error: workload statement verification required but Qhx-Workload-ID header not present
# Disable validation for non-QHx services
qhx curl --validate=false http://external-api.com/
# ✅ Success (no validation)

Comparison with standard curl:

Featurecurlqhx curl
HTTP requests✅✅
TLS/mTLS✅✅ (automatic via SPIFFE)
Workload identity❌✅
Request notarization❌✅
Offline verification❌✅ (with signed receipts)

Display current compartment context.

Usage:

Terminal window
qhx identity

Example output:

Compartment: US-TS-GESTALT
Country: US
Sensitivity: TS
Compartment: GESTALT
Kube Context: production-cluster
User: alice@example.com
User Groups: system:authenticated, qhx:country:us, qhx:sensitivity:ts, qhx:compartment:gestalt

Use case: Verify compartment before deploying classified workloads.


Apply Kubernetes resources with automatic MLS label injection.

Usage:

Terminal window
qhx apply -f FILENAME [flags]

Behavior:

  • Reads current compartment context
  • Injects mls.qhx.dev/level and mls.qhx.dev/compartment labels
  • Applies resources via Kubernetes API
  • Returns standard kubectl output

Examples:

Terminal window
# Apply deployment in current compartment
qhx apply -f deployment.yaml
# Apply with namespace override
qhx apply -f app.yaml -n my-namespace
# Apply from URL
qhx apply -f https://example.com/manifest.yaml
# Apply directory
qhx apply -f ./manifests/
# Dry-run to see what would be applied
qhx apply -f deployment.yaml --dry-run=client -o yaml

Label injection example:

Input YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: api:v1.0

After qhx apply (with context US-TS-GESTALT):

apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
labels:
qhx.io/compartment: US-TS-GESTALT # Injected
annotations:
mls.qhx.dev/level: us:ts # Injected
mls.qhx.dev/compartment: us:gestalt # Injected
spec:
replicas: 3
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
qhx.io/compartment: US-TS-GESTALT # Injected
annotations:
mls.qhx.dev/level: us:ts # Injected
mls.qhx.dev/compartment: us:gestalt # Injected
spec:
containers:
- name: api
image: api:v1.0

Retrieve resources with compartment information.

Usage:

Terminal window
qhx get RESOURCE [NAME] [flags]

Enhanced output: Adds COMPARTMENT column to standard kubectl output.

Examples:

Terminal window
# List all pods with compartment
qhx get pod
# Output:
# NAME READY STATUS RESTARTS COMPARTMENT
# us-api-7f8c9d-xyz 1/1 Running 0 US-TS-GESTALT
# uk-web-5b6d8a-abc 1/1 Running 0 UK-S-MARBLE
# au-db-9c4e2b-def 1/1 Running 0 AU-C-BASELINE
# Get specific pod details
qhx get pod us-api-7f8c9d-xyz -o yaml
# List deployments across all namespaces
qhx get deployment -A
# Watch pods in real-time
qhx get pod -w
# Filter by label
qhx get pod -l app=api
# Custom columns
qhx get pod -o custom-columns=NAME:.metadata.name,COMPARTMENT:.metadata.labels.qhx\\.io/compartment

Compartment filtering:

Terminal window
# Show only resources in current compartment
qhx get pod --filter-compartment
# Show resources in specific compartment
qhx get pod --compartment=UK-S-MARBLE

Execute commands in pods with compartment boundary enforcement.

Usage:

Terminal window
qhx exec POD_NAME [-c CONTAINER] -- COMMAND [args...]

Security: Prevents execution in pods outside current compartment.

Examples:

Terminal window
# Execute command in pod (same compartment)
qhx exec us-api-7f8c9d-xyz -- curl http://localhost:8080/health
# ✅ Success (same compartment)
# Attempt to execute in different compartment
qhx exec uk-web-5b6d8a-abc -- curl http://localhost:8080/health
# ❌ Error: Cannot execute in pod in another compartment (UK-S-MARBLE)
# Current compartment: US-TS-GESTALT
# Specify container
qhx exec us-api-7f8c9d-xyz -c sidecar -- ps aux
# Interactive shell
qhx exec -it us-api-7f8c9d-xyz -- /bin/bash
# Execute with stdin
echo "SELECT * FROM users;" | qhx exec us-db-pod -- psql -U admin

Override (admin only):

Terminal window
# Force execution in different compartment (requires special permission)
qhx exec uk-web-5b6d8a-abc --force -- curl http://localhost:8080/health

Retrieve logs from pods.

Usage:

Terminal window
qhx logs POD_NAME [-c CONTAINER] [flags]

Examples:

Terminal window
# Get logs from pod
qhx logs us-api-7f8c9d-xyz
# Follow logs
qhx logs -f us-api-7f8c9d-xyz
# Logs from specific container
qhx logs us-api-7f8c9d-xyz -c qhx-proxy
# Previous logs (from crashed container)
qhx logs us-api-7f8c9d-xyz --previous
# Last 100 lines
qhx logs us-api-7f8c9d-xyz --tail=100
# Logs since timestamp
qhx logs us-api-7f8c9d-xyz --since=1h

Delete resources (with compartment awareness).

Usage:

Terminal window
qhx delete RESOURCE NAME [flags]

Safety: Warns when deleting resources in different compartment.

Examples:

Terminal window
# Delete deployment
qhx delete deployment us-api
# Delete multiple resources
qhx delete pod us-api-xyz us-api-abc
# Delete by label
qhx delete pod -l app=old-version
# Delete from file
qhx delete -f deployment.yaml
# Force delete (skip warnings)
qhx delete deployment us-api --force

Show detailed information about resources.

Usage:

Terminal window
qhx describe RESOURCE NAME

Enhanced output: Highlights MLS labels and compartment information.

Example:

Terminal window
qhx describe pod us-api-7f8c9d-xyz
# Output includes:
# Name: us-api-7f8c9d-xyz
# Namespace: production
# Labels: app=api
# qhx.io/compartment=US-TS-GESTALT
# Annotations: mls.qhx.dev/level: us:ts
# mls.qhx.dev/compartment: us:gestalt
# ...

Forward ports from pods or services.

Usage:

Terminal window
qhx port-forward POD_NAME [LOCAL_PORT:]REMOTE_PORT [flags]

Examples:

Terminal window
# Forward port 8080 to localhost:8080
qhx port-forward us-api-7f8c9d-xyz 8080:8080
# Forward to different local port
qhx port-forward us-api-7f8c9d-xyz 9090:8080
# Forward multiple ports
qhx port-forward us-api-7f8c9d-xyz 8080:8080 9090:9090
# Forward from service
qhx port-forward service/us-api 8080:80

Copy files to/from containers.

Usage:

Terminal window
qhx cp SOURCE DEST [flags]

Examples:

Terminal window
# Copy from pod to local
qhx cp us-api-7f8c9d-xyz:/var/log/app.log ./app.log
# Copy from local to pod
qhx cp ./config.yaml us-api-7f8c9d-xyz:/etc/app/config.yaml
# Copy from specific container
qhx cp us-api-7f8c9d-xyz:/logs/error.log ./error.log -c sidecar

Manage compartment contexts.

Usage:

Terminal window
qhx context [SUBCOMMAND]

Subcommands:

List available contexts:

Terminal window
qhx context list
# Output:
# CURRENT NAME COUNTRY SENSITIVITY COMPARTMENT KUBE-CONTEXT
# * us-ts-gestalt US TS GESTALT prod-cluster
# uk-s-marble UK S MARBLE prod-cluster
# au-c-baseline AU C BASELINE staging-cluster

Switch to a different context:

Terminal window
# Switch context
qhx context use uk-s-marble
# Verify switch
qhx identity
# Output:
# Compartment: UK-S-MARBLE
# ...

Create a new context:

Terminal window
qhx context create jp-s-quantum \
--country=JP \
--sensitivity=S \
--compartment=QUANTUM \
--kube-context=production-cluster

Remove a context:

Terminal window
qhx context delete au-c-baseline

Show current context:

Terminal window
qhx context current
# Output: us-ts-gestalt

Filter resources by compartment:

Terminal window
# Show only pods in current compartment
qhx get pod --current-compartment
# Show pods in specific compartment
qhx get pod --compartment US-TS-GESTALT
# Show pods NOT in current compartment
qhx get pod --exclude-current-compartment

Preview changes without applying:

Terminal window
# See what labels would be injected
qhx apply -f deployment.yaml --dry-run=client -o yaml
# Server-side dry run
qhx apply -f deployment.yaml --dry-run=server
Terminal window
# Get pod as JSON
qhx get pod us-api-xyz -o json
# Get deployment as YAML
qhx get deployment us-api -o yaml
# Custom JSON path
qhx get pod us-api-xyz -o jsonpath='{.metadata.labels.qhx\.io/compartment}'
Terminal window
# Watch pods
qhx get pod -w
# Watch events
qhx get events -w
# Watch with timestamps
qhx get pod -w --show-labels=true

The qhx CLI is fully compatible with kubectl. Any kubectl command works with qhx:

Terminal window
# These are equivalent (with compartment context added):
kubectl get pod
qhx get pod
kubectl apply -f deployment.yaml
qhx apply -f deployment.yaml

Add to your shell configuration (~/.bashrc or ~/.zshrc):

Terminal window
# Use qhx by default
alias kubectl='qhx'
# Quick compartment switching
alias qhx-us='qhx context use us-ts-gestalt'
alias qhx-uk='qhx context use uk-s-marble'
alias qhx-au='qhx context use au-c-baseline'

Create compartment setup scripts:

us_compartment.sh
export QHX_COUNTRY=US
export QHX_SENSITIVITY=TS
export QHX_COMPARTMENT=GESTALT
PS1='(US-TS)$ '
# Source the script
source ./us_compartment.sh

Use case: Deploy the same application to multiple compartments

deploy-multi-compartment.sh
#!/bin/bash
for compartment in us-ts-gestalt uk-s-marble au-c-baseline; do
echo "Deploying to $compartment..."
qhx context use $compartment
qhx apply -f deployment.yaml
qhx rollout status deployment/api-server
done
echo "Deployment complete across all compartments"
# ~/.qhx/config.yaml (development)
currentContext: dev-unclassified
contexts:
dev-unclassified:
country: US
sensitivity: U
compartment: DEV
kubeContext: minikube
namespace: default
defaultUserGroups:
- system:authenticated
- qhx:country:us
- qhx:sensitivity:u
# ~/.qhx/config.yaml (production)
currentContext: us-ts-gestalt
contexts:
us-ts-gestalt:
country: US
sensitivity: TS
compartment: GESTALT
kubeContext: prod-us-west-1
namespace: classified-apps
us-s-default:
country: US
sensitivity: S
compartment: DEFAULT
kubeContext: prod-us-west-1
namespace: secret-apps
# Require explicit context switching
requireExplicitContextSwitch: true
# Audit log
auditLog:
enabled: true
path: ~/.qhx/audit.log

Issue: “Cannot execute in pod in another compartment”

Section titled “Issue: “Cannot execute in pod in another compartment””

Cause: Attempting to execute commands in a pod outside current compartment.

Solution:

Terminal window
# Check current compartment
qhx identity
# Switch to correct compartment
qhx context use <correct-compartment>
# Or use --force (admin only)
qhx exec <pod> --force -- <command>

Cause: Configuration not set or environment variables missing.

Check:

Terminal window
# Verify configuration
cat ~/.qhx/config.yaml
# Verify environment
env | grep QHX
# Test with dry-run
qhx apply -f test.yaml --dry-run=client -o yaml | grep mls.qhx.dev

Cause: Referenced context doesn’t exist in config.

Solution:

Terminal window
# List available contexts
qhx context list
# Create missing context
qhx context create <context-name> --country=... --sensitivity=... --compartment=...
Terminal window
# Before any operation
qhx identity
Terminal window
# Preview changes
qhx apply -f deployment.yaml --dry-run=client -o yaml | less
# Include compartment in resource names
metadata:
name: us-api-server # Prefix with compartment

Enable audit logging in production:

~/.qhx/config.yaml
auditLog:
enabled: true
path: ~/.qhx/audit.log
level: info # debug, info, warn, error
<country>-<sensitivity>-<compartment>
Examples:
- us-ts-gestalt
- uk-s-marble
- au-c-baseline
- jp-u-public
CommandDescriptionExample
qhx curlHTTP requests with notarizationqhx curl -n signRequest <url>
qhx identityShow current compartmentqhx identity
qhx applyApply with labelsqhx apply -f app.yaml
qhx getGet resourcesqhx get pod
qhx execExecute in podqhx exec pod -- cmd
qhx logsGet logsqhx logs pod
qhx deleteDelete resourcesqhx delete pod name
qhx describeDescribe resourceqhx describe pod name
qhx port-forwardForward portsqhx port-forward pod 8080
qhx cpCopy filesqhx cp pod:/path ./
qhx contextManage contextsqhx context list