zendesk-skill
CLAUDE.md8 starsChanged 20 days agoMITView on GitHub
A Claude Code skill that provides a CLI for Zendesk Support integration.
In this repositoryOne other file it gives its agents.
- zendeskSkill
The file
We split this CLAUDE.md into the rules it gives an agent
7 rules. About 0 are common, 1 is distinctive, and 6 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
6
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 117
Add command function in cli.py with @app.command() decorator
- Very specific: line 118
Use Typer's Annotated type hints for parameters
- Very specific: line 119
Call get_client() to get the Zendesk client
- Very specific: line 120
Use run_async() to call async client methods
- Very specific: line 123
Add named queries in queries.py if appropriate
- Very specific: line 124
Update SKILL.md with command documentation
- Distinctive: line 200
All commands output JSON to stdout:
≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Zendesk CLI - Developer Guide
A Claude Code skill that provides a CLI for Zendesk Support integration.
Project Structure
zendesk-skill/
├── pyproject.toml # uv project config (entry point: zd-cli)
├── SKILL.md # Claude Code skill file
├── CLAUDE.md # This file
├── README.md # User documentation
├── reference/ # Additional documentation
│ └── search-syntax.md # Zendesk search query reference
├── src/
│ └── zendesk_skill/
│ ├── __init__.py # Package init with key re-exports
│ ├── cli.py # Typer CLI with commands
│ ├── client.py # Zendesk API client (httpx-based) + secrets management
│ ├── crypto.py # Fernet encryption at rest (machine-derived key, HKDF)
│ ├── enrich.py # People / custom field name resolution
│ ├── export.py # Render a whole ticket as Markdown/HTML
│ ├── formatting.py # Markdown-to-HTML for write operations
│ ├── operations.py # Shared business logic (CLI + MCP server)
│ ├── reporting.py # Slack & Markdown report generation
│ ├── server.py # MCP server implementation
│ ├── storage.py # Response storage + structure extraction
│ ├── queries.py # jq query definitions
│ ├── utils/ # Shared utilities
│ │ ├── __init__.py
│ │ ├── time.py # Time formatting (mins_to_human)
│ │ └── security.py # Field wrapping for security
│ ├── scripts/ # Standalone analysis scripts
│ │ └── analyze_support_metrics.py
│ └── auth/ # Pluggable auth backends
│ ├── __init__.py # Package exports
│ ├── provider.py # AuthProvider protocol + resolve factory
│ ├── token_auth.py # Basic Auth (email + API token)
│ ├── oauth.py # OAuth 2.0 Authorization Code + PKCE
│ ├── server.py # Server-side OAuth token management
│ └── scopes.py # OAuth scope constants
└── tests/
├── test_basic.py # Basic + formatting tests
└── test_auth.py # Auth package testsArchitecture
CLI (cli.py)
- Uses Typer for command-line interface
- 25 commands organized by category
- Outputs JSON to stdout
- Saves full responses to
/tmp/zd-cli-$UID/
Auth (auth/)
AuthProviderprotocol: pluggable auth backendsresolve_auth_provider()factory: OAuth token → API token → errorTokenAuthProvider: Basic Auth with email + API tokenOAuthProvider: OAuth 2.0 Authorization Code + PKCE, auto-refresh- Lazy imports in provider.py to avoid circular deps with client.py
API Client (client.py)
- httpx-based async HTTP client
- Delegates auth to
AuthProvider(acceptsauth_providerparam) - Backwards compatible: still accepts explicit email/token/subdomain
- Proper error handling with actionable messages
Storage (storage.py)
- Saves all API responses to
/tmp/zd-cli-$UID/ - Auto-extracts response structure for metadata
- File naming:
{command}_{sha256_8}_{timestamp}.json
Queries (queries.py)
- Named jq queries per command type
- Executes jq via subprocess (safe)
- Supports both named and custom queries
Command Categories
- Ticket Commands (6): search, ticket, ticket-details, export-ticket, linked-incidents, attachment
- Write Operations (4): update-ticket, create-ticket, add-note, add-comment
- Metrics & Analytics (4): ticket-metrics, list-metrics, satisfaction-ratings, satisfaction-rating
- Views (3): views, view-count, view-tickets
- Users & Orgs (4): user, search-users, org, search-orgs
- Config (4): groups, tags, sla-policies, me
- Query (1): query (for jq queries on stored files)
Development
Running Commands
# Show all commands
uv run zd-cli --help
# Run specific command
uv run zd-cli me
uv run zd-cli search "status:open"
uv run zd-cli ticket-details 12345
# Get command help
uv run zd-cli search --helpTesting
# Run all tests
uv run pytest -v
# Test CLI import
uv run python -c "from zendesk_skill.cli import app; print('OK')"Adding a New Command
- Add command function in
cli.pywith@app.command()decorator - Use Typer's
Annotatedtype hints for parameters - Call
get_client()to get the Zendesk client - Use
run_async()to call async client methods - Save response with
save_response() - Output summary JSON with
output_json() - Add named queries in
queries.pyif appropriate - Update SKILL.md with command documentation
Command Pattern
@app.command("command-name")
def command_name(
arg: Annotated[str, typer.Argument(help="Description")],
option: Annotated[Optional[str], typer.Option("--option", "-o", help="Description")] = None,
output_path: Annotated[Optional[str], typer.Option("--output", help="Custom output path")] = None,
) -> None:
"""Command description."""
client = get_client()
try:
result = run_async(client.get("endpoint"))
file_path = save_response("command_name", {"arg": arg}, result, output_path)
output_json({
"summary_field": result.get("field"),
"file_path": str(file_path),
})
except ZendeskClientError as e:
output_error(str(e))Dependencies
typer>=0.9.0- CLI frameworkrich>=13.0.0- Pretty terminal outputhttpx>=0.27.0- Async HTTP clientpydantic>=2.0.0- Input validationmistune>=3.0.0- Markdown-to-HTML conversion for write operationscryptography>=42.0.0- Fernet encryption for secrets at rest
External Requirements
jqmust be installed forzd-cli querycommand to work- Zendesk credentials: OAuth token or API token (email + token + subdomain)
Auth Configuration
Auth provider is resolved automatically (first match wins):
- OAuth token on disk (
~/.config/zd-cli/oauth_token.json.enc) →OAuthProvider - API token (env vars or encrypted secrets) →
TokenAuthProvider
API token credentials load from:
- Environment variables:
ZENDESK_EMAIL,ZENDESK_TOKEN,ZENDESK_SUBDOMAIN - Config file (
config.json) for email/subdomain + encrypted secrets (secrets.json.enc) for token
OAuth client credentials load from:
- Environment variables:
ZENDESK_OAUTH_CLIENT_ID,ZENDESK_OAUTH_CLIENT_SECRET - Encrypted secrets file (
secrets.json.enc), managed viazd-cli auth set-oauth-client
Encryption at Rest
Secret files (API tokens, OAuth tokens, client secrets) are encrypted using Fernet symmetric encryption. The key is derived at runtime from machine-specific identifiers and never stored on disk.
- Module:
src/zendesk_skill/crypto.py - Key derivation:
machine_id + username + app_id→ HKDF-SHA256 → Fernet key - File convention:
foo.json→ encrypted asfoo.json.enc - Auto-migration: plaintext secrets in
config.jsonare auto-migrated tosecrets.json.encon first access - Opt-out: Set
ZD_ENCRYPTION=noneto disable encryption entirely - Salt: Auto-generated, stored in
config.json(plaintext config stays unencrypted)
Storage layout:
config.json— plaintext: email, subdomain, encryption_salt, channel, auth mode settingssecrets.json.enc— encrypted: token, oauth_client_id, oauth_client_secret, slack_webhook_urloauth_token.json.enc— encrypted: OAuth access/refresh tokensserver_token.json.enc— encrypted: relay server JWT
CLI commands for managing encrypted credentials:
zd-cli auth set-oauth-client- Save OAuth client_id and client_secret (encrypted)zd-cli auth login- Save API token credentials (token encrypted, email/subdomain in config)
Response Format
All commands output JSON to stdout:
{
"summary_field": "value",
"file_path": "/tmp/zd-cli-$UID/command_xxx.json"
}The file_path points to the full saved response for follow-up queries.
MCP Server Backup
The original MCP server implementation is preserved in backup-mcp/server.py in case it's needed in the future. The CLI approach was chosen for easier integration with Claude Code skills (no settings.json configuration required).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

