This project follows a test-driven development approach for implementing the Claude Code JSON protocol:
- Discover Protocol Through Usage: Run examples to interact with Claude and discover new message types
- Capture Failed Cases: Any JSON that fails to deserialize is automatically saved to
test_cases/failed_deserializations/ - Format Test Cases: Run
./format_test_cases.shto ensure JSON formatting - Implement Missing Types: Add the necessary variants to
ClaudeOutputenum insrc/io.rs - Verify Implementation: Run
cargo test deserializationto ensure the new types deserialize correctly - Lock in Progress: Successful test cases prove our protocol implementation is correct
Claude Code ships no published schema — the wire types live as minified zod definitions inside the Bun-compiled CLI ELF. Two scripts turn that into a drift signal:
# Full extraction (one block per schema, for the crate-side field-by-field diff):
python3 scripts/extract_claude_sdk_schemas.py -o /tmp/claude_sdk_schemas.txt
# Automated drift check against the committed snapshot (wire labels + top-level fields):
python3 scripts/check_claude_schema_drift.py # exit 0 clean / 1 drift / 2 skip
python3 scripts/check_claude_schema_drift.py --update # accept a new snapshotThe snapshot lives at claude-codes/tests/schemas/claude_stream_json_snapshot.txt
and the nightly .github/workflows/claude-schema-drift.yml opens a
claude-schema-drift-labelled issue when the installed CLI drifts from it.
Full procedure — where the bundle lives, how to map schemas to crate types,
what counts as drift, manual spelunking recipes — is in
claude-codes/RESNAPSHOTTING.md.
CRITICAL: This repository enforces a strict PR-based workflow
# Clone the repository
git clone https://github.com/meawoppl/rust-claude-codes
cd rust-claude-codes
# Install git hooks (required for all contributors)
./setup_hooks.sh
# Create a feature branch for your work
git checkout -b feature/your-feature-name
# Make your changes, then commit (avoid git add -A!)
git add -u # Stage modified files only
git commit -m "Your descriptive commit message"
# Push to GitHub and create a PR
git push origin feature/your-feature-name- NEVER commit directly to main branch
- All changes MUST go through feature branches and pull requests
- The pre-commit hook will block direct commits to main
The repository has git hooks that enforce:
- No commits to main branch
- Code formatting with
cargo fmt - All clippy warnings resolved
- All tests passing
- JSON test cases properly formatted
If you haven't already, run ./setup_hooks.sh to install these hooks.
All PRs must pass the following GitHub Actions checks:
- ✅ Code formatting (
cargo fmt --all -- --check) - ✅ Clippy linting (
cargo clippy --all-targets --all-features -- -D warnings) - ✅ All tests passing (
cargo test --all-features) - ✅ JSON test cases properly formatted
- ✅ All examples compile (
./build_examples.sh) - ✅ Documentation builds (
cargo doc --no-deps) - ✅ MSRV compatibility (Rust 1.85+)
When implementing new message types:
- Start with the test case - Look at the failed JSON in
test_cases/ - Identify the structure - Note field names, types, and nesting
- Add to ClaudeOutput enum - Create a new variant with appropriate struct
- Follow existing patterns - Use
#[serde(skip_serializing_if = "Option::is_none")]for optional fields - Test immediately - Run
cargo test deserializationto verify - Document the type - Add doc comments explaining when this message appears
The reference/ directory (gitignored) holds local clones of other codex/claude SDK consumers, kept around to study their serialization patterns when evolving our own. Refresh with git -C reference/<repo> pull as needed; not part of the workspace build.
codex-rs/app-server-protocol/src/protocol/v{1,2}.rs— Hand-written serde-annotated Rust structs for every wire type.schemarsderives produce a JSON Schema.codex-rs/app-server-protocol/src/bin/{export,write_schema_fixtures}.rs— Binaries that emit the schema. The shippedcodexCLI exposes this ascodex app-server generate-json-schema --out DIR(emitscodex_app_server_protocol.v2.schemas.json) andcodex app-server generate-tsfor TypeScript.sdk/python/src/openai_codex/generated/v2_all.py— 8,295 lines, 128 notification types auto-generated bydatamodel_code_generatorfrom the JSON Schema. Pipeline lives atsdk/python/scripts/update_sdk_artifacts.py:517-532. The matching dispatch table is atgenerated/notification_registry.py.sdk/python/src/openai_codex/client.py:449-460— Dispatch logic. On unknown method OR typed-decode failure, routes toUnknownNotification(params=params_dict)(silent downgrade, no error).
rpc/notifications_gen.go— Generated dispatch map; unmarshal errors are caught and the function returnsNotification{Method, Raw: params}, nil. Raw frame preserved, but errors are silently swallowed.internal/codegen/main.go— Their codegen entry-point, same JSON Schema source.
src/app-server/rpc/client.ts:656-665— Zod-validated dispatch. On validation failure, silently drops the notification with a warn log. Weakest design of the four.
lib/codex/app_server/notification_adapter.ex:499-505— Hand-written pattern matching withEvents.AppServerNotification{method, params}catch-all. Similar to ourUnknown { method, params }fallback.
| SDK | Unknown method | Typed-decode failure | Raw frame preserved on failure |
|---|---|---|---|
| Python (official) | Silent downgrade to UnknownNotification |
Silent downgrade (same path) | In .payload.params |
| Go | Silent downgrade with .Raw |
Silent (error swallowed) | In .Raw |
| TypeScript | Dropped entirely | Dropped | No |
| Elixir | Silent downgrade | Falls back to raw | In params |
Our codex-codes |
Silent → Unknown { method, params } |
Errors loudly via Error::Deserialization(ParseError) |
Yes (ParseError.raw_line, .raw_json, .method) |
Ours is the strictest and most informative — but also the least forgiving of server version skew. See "prioritized improvements" below.
- Consume the upstream JSON Schema rather than hand-writing types. Either codegen against
codex_app_server_protocol.v2.schemas.jsonlike the Python SDK, or — cleaner since we're also Rust — depend on theapp-server-protocolcrate directly and re-export. We currently model ~15 notification variants; Python has 128. Big ~88% coverage gap. - Optionally downgrade typed-decode failures to
Unknownbehind a flag, instead of bubblingError::Deserialization. Match Python/Go/Elixir behaviour for forward-compat-by-default, but keep our strictParseErrorpath opt-in for bug-report fidelity. - Track the server's reported version from the
initializeresponse and expose it asclient.server_version() -> Option<&str>. Log on startup; warn on significant skew. Currently we don't surface it. - Audit
FileUpdateChange/PatchChangeKindagainstcodex-rstypes — issue #128 noted atypevalue not inadd|delete|update. Likely our enum is missing variants the wire now sends. - Add the obviously-missing notifications the Python registry has:
item/fileChange/patchUpdated,turn/plan/updated,turn/diff/updated,item/plan/delta, MCP-related (mcpServer/oauthLogin/completed), account-related (account/login/completed). - Document the
Unknownvariant contract inmessages.rs— make explicit that callers should ignore unknowns rather than treat them as errors, so version skew remains a soft failure.
IMPORTANT: Before every commit, you MUST:
- Run
cargo fmt --allto format all Rust code - Run
cargo clippy --all-targets --all-features -- -D warningsand fix all warnings - Ensure all tests pass with
cargo test --all
CRITICAL: ALWAYS run cargo fmt --all and cargo clippy --all-targets --all-features -- -D warnings before EVERY commit without exception. This is non-negotiable.
NOTE: CI will fail if there are clippy warnings or formatting issues, so please fix them before committing.
CRITICAL: The git add -A command is STRICTLY PROHIBITED in this repository!
WHY THIS MATTERS:
git add -Astages ALL files including untracked files, temp files, build artifacts, and other random crap- This has repeatedly caused issues with unwanted files being committed
- It can expose sensitive information, break builds, and pollute the repository
WHAT TO USE INSTEAD:
git add -u- Stages only modified tracked files (PREFERRED)git add <specific-file>- Stage specific files by namegit add src/- Stage specific directories if needed- ALWAYS run
git statusfirst to review what will be staged
UNACCEPTABLE:
git add -A # NEVER DO THIS
git add --all # NEVER DO THIS
git add . # AVOID THIS TOOCORRECT:
git status # Review changes first
git add -u # Stage modified files only
git add src/io.rs # Or stage specific filesRemember: It's better to run git add multiple times for specific files than to accidentally commit garbage with -A.
- Follow Rust naming conventions (snake_case for functions/variables, CamelCase for types)
- Use descriptive variable names
- Keep functions focused and under 50 lines when possible
- Document public APIs with rustdoc comments
- Place shared code in appropriate modules to avoid duplication
- Use Result types for fallible operations
- Provide meaningful error messages
- Implement proper error propagation with
?operator - Consider using
thiserrororanyhowfor error management
- Write unit tests for all business logic
- Use
#[cfg(test)]modules for test code - Mock external dependencies in tests
- Aim for high test coverage
- Use property-based testing with
quickcheckorproptestwhere applicable
- Profile before optimizing
- Use
&strinstead ofStringwhen ownership isn't needed - Prefer iterators over collecting into intermediate vectors
- Use
Cowfor potentially-borrowed data - Consider using
ArcorRcfor shared ownership when appropriate
- Use
tokiofor async runtime when needed - Properly handle async errors
- Avoid blocking operations in async contexts
- Use
tokio::spawnfor concurrent tasks
When I say:
- "complete": Run
cargo fmt --all, fix clippy issues withcargo clippy --all-targets --all-features -- -D warnings, then commit and push - "freshen": Pull main and merge into the current branch
- "merge main": Pull the remote main branch and merge it into the current working branch
- "integration tests": Run
cargo test --features integration-tests(requires Claude CLI installed and API key configured)
- Build from smallest testable pieces
- Validate each component before integration
- Layer functionality incrementally
- Maintain working state at each step
- Don't take shortcuts that compromise quality
- Handle edge cases properly
- Consider error paths thoroughly
- Write code that's maintainable and clear
- Write tests alongside implementation
- Test edge cases and error conditions
- Ensure tests are deterministic and reliable
- Keep tests focused and independent
- Don't use esoteric scripts to edit code - use direct read/write operations
- Always run fmt and clippy before committing
- Never create files unless absolutely necessary
- Prefer editing existing files over creating new ones
- Update documentation when making significant changes
When updating the version number in Cargo.toml:
- Update the version field in
Cargo.toml - Run
cargo buildto regenerateCargo.lockwith the new version - Commit both
Cargo.tomlandCargo.locktogether - Use a commit message like: "chore: bump version to X.Y.Z"
This ensures the lockfile stays in sync with the version number.
Patch bumps (0.x.Y → 0.x.Y+1, or x.y.Z → x.y.Z+1) are fine to do without asking.
For anything larger — minor (0.x → 0.x+1, or x.y → x.y+1) or major (x → x+1) — ask first and wait for explicit approval before bumping. These reflect breaking or significant API changes and the user wants to be in the loop on the framing / changelog narrative before the version moves.
- NEVER use
cargo publish --allow-dirty— ensure the working tree is clean before publishing - If there are untracked files, either
.gitignorethem or clean them up first
When adding or updating dependencies:
- ALWAYS use
cargo addorcargo removecommands instead of manually editingCargo.toml - For optional dependencies:
cargo add <package> --optional - To update all dependencies:
cargo update - To update specific dependency:
cargo update <package>
Examples:
cargo add serde --features derive # Add with features
cargo add tokio --optional # Add as optional
cargo remove old-package # Remove a dependency
cargo update # Update all to latest compatible