SDKs
Custom code technical reference
Note
This reference describes the internal implementation of custom code. Understanding these details is not required to use the feature - this information is for debugging, troubleshooting, or satisfying technical curiosity.
How the 3-way merge works
Section titled “How the 3-way merge works”Custom code uses a 3-way merge algorithm similar to Git merge. For each file, three versions are tracked:
graph LR
A[Base<br/>Last pristine generation] --> D[3-Way Merge]
B[Current<br/>Modified version on disk] --> D
C[New<br/>Latest generation] --> D
D --> E[Result<br/>Merged output]
The merge process:
- Base: The pristine version from the last generation (stored in Git objects)
- Current: The version on disk (potentially with custom changes)
- New: The newly generated version from Speakeasy
If changes do not overlap, the merge completes automatically. When changes overlap, Speakeasy writes conflict markers to the file and stages the conflict in Git’s index using three-stage entries (base, ours, theirs). This ensures git status, IDEs, and other Git tools recognize the conflict and prompt for resolution.
File tracking and move detection
Section titled “File tracking and move detection”Generated file headers
Section titled “Generated file headers”Each generated file contains a tracking header:
// Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.// @generated-id: a1b2c3d4e5f6The @generated-id is a deterministic hash based on the file’s original path. This allows Speakeasy to detect when files are moved:
- File generated at
src/models/user.tsgets ID based on that path - File is moved to
src/entities/user.ts - Next generation, Speakeasy scans for the ID and finds it at the new location
- Updates are applied to the moved file
Lockfile structure
Section titled “Lockfile structure”The .speakeasy/gen.lock file tracks generation state:
{ "generationVersion": "2.500.0", "persistentEdits": { "generation_id": "abc-123", "pristine_commit_hash": "deadbeef123", "pristine_tree_hash": "cafebabe456" }, "trackedFiles": { "src/models/user.ts": { "id": "a1b2c3d4e5f6", "last_write_checksum": "sha1:7890abcdef", "pristine_git_object": "blobhash123", "moved_to": "src/entities/user.ts" } }}Fields explained:
generation_id: Unique ID for this generationpristine_commit_hash: Git commit storing pristine generated codepristine_tree_hash: Git tree hash for no-op detectiontrackedFiles: Per-file tracking informationid: File’s generated ID for move detectionlast_write_checksum: Checksum when last written (dirty detection)pristine_git_object: Git blob hash of pristine versionmoved_to: New location if file was moved
Git integration details
Section titled “Git integration details”Object storage
Section titled “Object storage”Custom code stores pristine generated code using Git’s object database:
- Blob objects: Each generated file is stored as a blob
- Tree objects: Directory structure is stored as trees
- Commit objects: Each generation creates a commit
These objects live in .git/objects but are not referenced by any branch.
Git refs
Section titled “Git refs”Generation snapshots are stored at:
refs/speakeasy/gen/<generation_id>These refs:
- Are not visible in
git branchor GitHub - Keep objects reachable so
git gcdoes not prune them - Can be fetched/pushed like any other ref
Git commands used
Section titled “Git commands used”The Speakeasy CLI uses the following Git operations internally:
| Operation | Git command |
|---|---|
| Store file content | git hash-object -w --stdin |
| Create tree | git write-tree |
| Create commit | git commit-tree |
| Read pristine content | git cat-file -p <hash> |
| Check object exists | git cat-file -e <hash> |
| Stage conflict | git update-index --index-info |
| Fetch snapshot | git fetch origin refs/speakeasy/gen/<id> |
| Push snapshot | git push origin <commit>:refs/speakeasy/gen/<id> |
When conflicts are detected, Speakeasy stages three versions of the file in Git’s index (stages 1, 2, and 3 representing base, ours, and theirs). This is the same mechanism Git uses for merge conflicts, ensuring compatibility with standard Git conflict resolution tools.
Edge cases and behavior
Section titled “Edge cases and behavior”File moves before enabling
Section titled “File moves before enabling”If files are moved before enabling custom code:
- No
@generated-idheader exists - Move cannot be detected
- File treated as deleted at old location, created at new location
- History is broken for that file
Recommendation: Enable custom code before reorganizing files.
Duplicate IDs
Section titled “Duplicate IDs”If a file is copied (not moved):
- Both files have the same
@generated-id - System detects duplicate and logs warning
- Prefers the file at the expected (original) location
- Other copies may not receive updates correctly
Deleted files
Section titled “Deleted files”When a generated file is deleted:
- Marked as
deleted: truein lockfile - Speakeasy does not recreate it
- To restore: remove the file entry from
trackedFilesin gen.lock
Binary files
Section titled “Binary files”Binary files (images, JARs, etc.) are handled differently:
- No
@generated-idheader (cannot add comments) - Tracked by path only (no move detection)
- Replaced entirely on regeneration if changed
Important
Regeneration completely overwrites binary files. Any manual modifications to binary files are lost.
CI mode detection
Section titled “CI mode detection”Custom code runs in two modes:
- Interactive mode: when stdin is a TTY and no explicit CI mode is set. Prompts like “Enable custom code?” are shown.
- CI mode: when running non-interactively (no TTY) or when
mode: ciis configured in the SDK generation action or CLI environment. Prompts are suppressed and conflicts cause a non-zero exit.
In GitHub Actions, the speakeasy-api/sdk-generation-action automatically sets the appropriate mode.
Troubleshooting
Section titled “Troubleshooting”Common issues
Section titled “Common issues””Failed to fetch pristine snapshot”
Section titled “”Failed to fetch pristine snapshot””This occurs when:
- Remote repository is unreachable
- CI does not have permission to fetch refs
Solution: Ensure full clone in CI:
- uses: actions/checkout@v4 with: fetch-depth: 0 # Full clone, not shallow“Duplicate generated ID detected”
Section titled ““Duplicate generated ID detected””This means multiple files have the same @generated-id:
- Check if files were copied instead of moved
- Remove duplicate headers from copied files
- Let Speakeasy assign new IDs on next generation
”Cannot resolve conflicts automatically”
Section titled “”Cannot resolve conflicts automatically””Manual changes and Speakeasy updates modify the same lines:
- Open the conflicted files
- Resolve conflicts manually (keep desired code)
- Run
speakeasy run --skip-versioning - Commit the resolution
The --skip-versioning flag tells Speakeasy to reuse the existing pristine snapshot (the “Base” in the 3-way merge) instead of creating a new snapshot from the conflicted state. This keeps the merge inputs stable while resolving conflicts.
Resetting to pristine state
Section titled “Resetting to pristine state”Full reset (low-level)
Section titled “Full reset (low-level)”To discard all customizations and also reset custom code internal tracking:
- Delete all generated files
- (Optional, advanced) Remove the
persistentEditssection from.speakeasy/gen.lock - Run
speakeasy runto generate fresh files and create a new snapshot
For a higher-level reset workflow that does not require editing internal tracking files, see the “How to reset to pristine generated code” FAQ in the main custom code guide.
To temporarily disable without losing configuration:
persistentEdits: enabled: false # or "never" to prevent promptsInspecting Git objects
Section titled “Inspecting Git objects”To debug custom code internals:
# View stored refsgit show-ref | grep refs/speakeasy/gen/
# Inspect a specific generationgit log --oneline refs/speakeasy/gen/<generation_id>
# See pristine version of a filegit show <pristine_git_object_hash>
# Check if object exists locallygit cat-file -e <hash> && echo "exists" || echo "missing"Performance considerations
Section titled “Performance considerations”Repository size
Section titled “Repository size”Generation snapshots use a commit history (each new generation commit has the previous as its parent), enabling Git delta compression. This keeps storage efficient:
- Git pack files compress content efficiently using deltas between generations
- Identical files share storage (deduplication)
- Only the latest generation needs to be fetched
- Old generations can be pruned if needed
Initial scan
Section titled “Initial scan”On first run with existing modifications:
- Speakeasy scans all files for
@generated-idheaders - This can be slow for very large SDKs (1000+ files)
- Subsequent runs use cached information from lockfile
Merge performance
Section titled “Merge performance”The 3-way merge is performed per-file:
- Clean files (no changes) are fast - just overwrite
- Modified files require diff computation
- Conflicts are rare in practice
Security considerations
Section titled “Security considerations”Git permissions
Section titled “Git permissions”Custom code requires:
- Read access to
.git/objects - Ability to create Git objects locally
- Optional: push access to
refs/speakeasy/gen/*(soft failure if unavailable)
Remote operations
Section titled “Remote operations”- Fetch: Attempts to get latest pristine snapshot
- Push: Attempts to backup new snapshot
- Both operations are “fire and forget”; failures do not block generation
No branch access
Section titled “No branch access”Custom code never:
- Creates or modifies branches
- Changes commit history
- Modifies the working branch
- Requires access to source branches
Language-specific behavior
Section titled “Language-specific behavior”Comment syntax
Section titled “Comment syntax”Different languages use different comment styles for the @generated-id header:
| Language | Comment style |
|---|---|
| Go, Java, JavaScript, TypeScript, C# | // @generated-id: abc123 |
| Python, Ruby, Shell | # @generated-id: abc123 |
| HTML, XML | <!-- @generated-id: abc123 --> |
| CSS | /* @generated-id: abc123 */ |
Files without comment support
Section titled “Files without comment support”Some file types do not support comments:
- JSON files
- Binary files
- Some configuration formats
These files:
- Cannot have
@generated-idheaders - Are tracked by path only
- Do not support move detection