Fixing CLI Error Handling: A Deep Dive into Keyshade's WebSocket Communication Bug


“If you build software, every error message is marketing” - Jason Fried, Rework
Introduction
In today’s fast-paced software development landscape, managing credentials and sensitive configurations securely is more critical—and more challenging—than ever. Dispersed secrets, manual processes, and unplanned storage not only slow teams down but also increase the risk of breaches and operational errors. Keyshade addresses these challenges head-on with a centralized, automated approach to credential and configuration governance. Purpose-built for engineering teams, DevOps practitioners, startups, and open-source maintainers. Keyshade streamlines secret management while enhancing security, collaboration, and efficiency.
Keyshade is an advanced platform for centralized credential and configuration governance, designed to safeguard sensitive data, eliminate human error, and streamline operational complexity. By automating secret management, it fortifies security posture, amplifies cross-team collaboration, and liberates developers from the inefficiencies of fragmented secret storage. Its primary constituency—engineering teams, DevOps practitioners, early-stage startups, and open-source maintainers—adopts Keyshade to mitigate secret proliferation, minimize security liabilities, and embed robust secret orchestration seamlessly into their development pipelines. Users gravitate toward Keyshade for its ability to render secret management secure, intuitive, and collaborative, without imposing friction upon established workflows or toolchains. Below is a flow of control diagram discussing how users would typically use the product:
The user can send commands via keyshades terminal or on keyshades web app
On the web, the user can sign up, and initialize projects within workspaces
- Users can do the same in the CLI
Once the a project is made a http request is made to the API websocket service
The websocket service then retrieves that data to put in the Keyshade’s database to save the data for future use
Keyshade is currently in alpha and not live so there really are not any users as of posting this.
The issue
I partnered with a member of the Computing Talent Initiative with the task of solving issue 998 in Keyshade for a Summer Open Source Experience sponsored by . The main problem we noticed was that the Keyshade command line interface’s “keyshade run” command fails to give a proper error that users can understand. We would want to extract an error message and output it to the user. The issue is a bug that seems urgent to solve right now for users to understand errors they might make when working with Keyshade’s CLI, which is the main feature of Keyshade. Here are some key locations in the codebase we found out to be useful in solving the issue:
run.command.ts
Handles CLI logic for running commands, including WebSocket client interactions
Within a CLI folder
change-notifier.socket.ts
Contains the handleRegister method, which processes the register-client-app event
Handles authentication, authorization, and client registration logic
Emits the client-registered response to the client and logs errors
Within an API folder
run.types.d.ts
ClientRegisteredResponse describes the shape of the response sent by the server after registration (success, message)
Ensures type safety for client-side code handling server responses
Within a CLI folder
Here are steps regarding how we reproduced the problem before finding the important locations in the codebase
Steps to reproduce the behaviour:
Create a project on keyshade
Configure your CLI to tap into this project
Delete the project
Use keyshade run <> command
See the error
Expected behaviour
We would want the CLI to display what actually went wrong. The API should send the formatted message to the CLI. The CLI should print the error.
Keyshade’s Codebase
Before delving into the problem itself, and after pinpointing the critical components, we took a step back to develop a comprehensive understanding of the codebase’s architecture. Keyshade’s backend is architected with NestJS (a progressive Node.js framework) and TypeScript, leveraging Prisma as its database ORM, interfacing with a PostgreSQL relational database. It also integrates Redis for high-performance caching and publish/subscribe messaging, as well as Socket.IO for real-time, bidirectional communication between its command line interface and application programming interface.
On the frontend, the stack comprises React, TypeScript, JavaScript, and Next.js, presumably to power a performant and SEO-friendly web interface. Development workflows are streamlined through Nx (for monorepo orchestration), ESLint and Prettier (for code quality and style enforcement), alongside the TypeScript compiler for rigorous static type checking.
For security, Keyshade employs Elliptic Curve Cryptography for robust public-key encryption, JWT for token-based authentication, and API key authentication for service-to-service communication. The infrastructure is containerized via Docker and designed with cloud deployment in mind. A high-level system diagram encapsulates the overall architecture of the codebase as follows:
Here is a use case, using the components in the diagram to give you a better understanding of it.
Use Case: Developer Fetching Live Configuration Updates
Step 1: Developer Initiates CLI Command
keyshade run \--workspace my-company \--project web-app \--environment production
What happens: The developer runs the CLI command to start their application with live configuration updates from Keyshade.
The CLI documentation can be found [here](https://docs.keyshade.xyz/getting-started/installing-the-cli)
Step 2: CLI Processes Command (run.command.ts)
Parses the command arguments (workspace, project, environment)
Reads the local configuration file to get API credentials
Validates the required parameters using types from run.types.d.ts
Prepares to establish WebSocket connection for live updates
Step 3: WebSocket Connection Establishment
The CLI creates a websocket connection:
const socket = io('ws://api.keyshade.xyz/change-notifier') |
What happens: The client connects to the NestJS backend's WebSocket gateway (change-notifier.socket.ts).
Step 4: Client Registration Request
The CLI emits the registration event:
socket.emit('register-client-app', { workspaceSlug: 'my-company', projectSlug: 'web-app', environmentSlug: 'production'}); |
What happens: This triggers the handleRegister method in change-notifier.socket.ts
Step 5a: Authentication
const userContext = await this.extractAndValidateUser(client)
Extracts API key from WebSocket headers (x-keyshade-token)
Queries PostgreSQL database via Prisma to validate the API key
- Verifies user account is active
Step 5b: Authorization checks
await this.authorizationService.authorizeUserAccessToWorkspace({...})await this.authorizationService.authorizeUserAccessToProject({...})await this.authorizationService.authorizeUserAccessToEnvironment({...}) |
Checks if user has READ_WORKSPACE, READ_VARIABLE, READ_SECRET permissions
Validates access to the specific project
Confirms environment access rights
Step 5c: Client Registration
await this.addClientToEnvironment(client, environment.id)
Creates entry in PostgreSQL mapping socket ID to environment ID
- Adds client to Redis set for real-time notifications
Step 6: Success Response
What happens: Server sends confirmation back to CLI using the ClientRegisteredResponse interface from run.types.d.ts.
Step 7: Configuration Fetching
After successful registration:
CLI fetches initial configuration values (secrets, variables)
Application starts with these environment variables
CLI continues listening for real-time updates
Step 8: Real-Time Updates (Ongoing)
Another developer updates a secret value through the web interface.
await this.redisSubscriber.subscribe(CHANGE\_NOTIFIER\_RSC, this.notifyConfigurationUpdate.bind(this))
The web interface publishes change to Redis channel
All API instances receive the notification
this.server.to(clientId).emit('configuration-updated', data)
Server identifies all clients registered for that environment
Sends update notification to active CLI instances
CLI receives the configuration-updated event
Updates local environment variables
Application continues running with new values
Challenges
One specific technical challenge I encountered while attempting to solve issue 998 was figuring out why my changes in change-notifier.socket.ts were not being reflected when I tested them through the CLI. Despite updating the logic inside the Socket.IO handlers, the CLI behavior remained unchanged, and it was unclear whether the API changes were even being registered. Here is an attempt to solve this challenge and other challenges I encountered.
So I tried debugging the issue after reproducing the issue and found the debug information for keyshade issue 998
@johnny603 ➜ /workspaces/keyshade (teamAttempt/#998) $ DEBUG=* keyshade run "echo 'test'"INFO: Checking API key validity...INFO: API key is valid!INFO: Connecting to socket...socket.io-client:url parse wss://api.keyshade.xyz/change-notifier +0mssocket.io-client new io instance for wss://api.keyshade.xyz/change-notifier +0mssocket.io-client:manager readyState closed +0mssocket.io-client:manager opening wss://api.keyshade.xyz/change-notifier +0msengine.io-client:socket creating transport "websocket" +0msengine.io-client:socket options: {"path":"/socket.io/","agent":false,"withCredentials":false,"upgrade":true,"timestampParam":"t","rememberUpgrade":false,"addTrailingSlash":true,"rejectUnauthorized":true,"perMessageDeflate":{"threshold":1024},"transportOptions":{},"closeOnBeforeunload":false,"autoConnect":false,"extraHeaders":{"x-keyshade-token":"ks_a3adff2a9db8bf3c42fc817880ac4125983b83c171f0d04d"},"transports":[null],"hostname":"api.keyshade.xyz","secure":true,"port":"443","query":{"EIO":4,"transport":"websocket"},"socket":{"binaryType":"nodebuffer","writeBuffer":[],"_prevBufferLen":0,"_pingInterval":-1,"_pingTimeout":-1,"_maxPayload":-1,"_pingTimeoutTime":null,"secure":true,"hostname":"api.keyshade.xyz","port":"443","transports":["websocket"],"_transportsByName":{},"opts":{"path":"/socket.io/","agent":false,"withCredentials":false,"upgrade":true,"timestampParam":"t","rememberUpgrade":false,"addTrailingSlash":true,"rejectUnauthorized":true,"perMessageDeflate":{"threshold":1024},"transportOptions":{},"closeOnBeforeunload":false,"autoConnect":false,"extraHeaders":{"x-keyshade-token":"ks_a3adff2a9db8bf3c42fc817880ac4125983b83c171f0d04d"},"transports":[null],"hostname":"api.keyshade.xyz","secure":true,"port":"443"},"readyState":"opening"}} +0msengine.io-client:socket setting transport websocket +6mssocket.io-client:manager connect attempt will timeout after 20000 +8msengine.io-client:socket socket receive: type "open", data "{"sid":"qxJTaYpvOE4a6kIaAAB8","upgrades":[],"pingInterval":25000,"pingTimeout":20000,"maxPayload":1000000}" +756msengine.io-client:socket socket open +0mssocket.io-client:manager open +755mssocket.io-client:manager cleanup +0mssocket.io-client:socket transport is open - connecting +0mssocket.io-client:manager writing packet {"type":0,"nsp":"/change-notifier"} +1mssocket.io-parser encoding packet {"type":0,"nsp":"/change-notifier"} +0mssocket.io-parser encoded {"type":0,"nsp":"/change-notifier"} as 0/change-notifier, +0msengine.io-client:socket flushing 1 packets in socket +1msengine.io-client:socket starting upgrade probes +1msengine.io-client:socket socket receive: type "message", data "0/change-notifier,{"sid":"OpbhAQ0UYcfklhBoAAB9"}" +249mssocket.io-parser decoded 0/change-notifier,{"sid":"OpbhAQ0UYcfklhBoAAB9"} as {"type":0,"nsp":"/change-notifier","data":{"sid":"OpbhAQ0UYcfklhBoAAB9"}} +251mssocket.io-client:socket socket connected with id OpbhAQ0UYcfklhBoAAB9 +251mssocket.io-client:manager writing packet {"type":2,"data":["register-client-app",{}],"options":{"compress":true},"nsp":"/change-notifier"}+251mssocket.io-parser encoding packet {"type":2,"data":["register-client-app",{}],"options":{"compress":true},"nsp":"/change-notifier"} +0mssocket.io-parser encoded {"type":2,"data":["register-client-app",{}],"options":{"compress":true},"nsp":"/change-notifier"} as 2/change-notifier,["register-client-app",{}] +0msengine.io-client:socket flushing 1 packets in socket +1mssocket.io-client:socket draining queue +1msengine.io-client:socket socket receive: type "message", data "2/change-notifier,["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}]" +261mssocket.io-parser decoded 2/change-notifier,["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}] as {"type":2,"nsp":"/change-notifier","data":["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}]} +262mssocket.io-client:socket emitting event ["client-registered",{"success":false,"message":{"response":{"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","error":"Not Found","statusCode":404},"status":404,"options":{},"message":"{\"header\":\"Workspace not found\",\"body\":\"Workspace undefined not found\"}","name":"NotFoundException"}}] +261msERROR: Error registering to API: [object Object] |
I came to the conclusion that the client is sending an empty object {} in the registration message, but the server expects a ChangeNotifierRegistration object with workspace, project, and environment slugs. This results in undefined values being passed to the authorization check, causing the “Workspace undefined not found” error. The code correctly creates the registrationData object with workspaceSlug, projectSlug, and environmentSlug
But the debug output shows it’s sending an empty object {}
This suggests that the data parameter being passed to connectToSocket has undefined values for workspace, project, and environment. Our team reached out to the issue maintainer radjip-b with a question regarding our process of the issue so far.
This small technical challenge was solved.
Solution
The solution for Keyshade’s issue 998 took five and a half weeks to complete during the eight-week internship. In the remaining time, we addressed additional related issues, including issue 989 and 988, which involved adding Slack and Discord integration documentation, as well as contributing to PR 1093.
Changes Introduced by PR #1093:
Slack Integration Documentation: A brand-new “How It Works” modal was added to the Keyshade platform’s UI, providing a visual, step-by-step guide—including screenshots—on how to set up Slack integration. This includes selecting Slack in the Integrations tab, entering credentials (signing secret, Bot token, channel ID), and testing with sample secrets and variables.
Modal Improvements & Developer Feedback: The modal component underwent adjustments—such as replacing a bare <DialogClose asChild /> (which caused rendering issues) with a proper close button, and revising instructions for better clarity (“Bot Token from Slack”). Additionally, an unused function (_handleClose) was cleaned up and textual instructions were streamlined.
Discord Integration Documentation: The scope of the PR was expanded to include documentation for Discord integration as well—hence the updated title to “feat: added slack and discord integration docs.”
Internal Routing and Formatting Refactor: Responding to reviewer feedback, the team refactored the integration documentation routing logic—removing the DOCS_SLUG_MAP, simplifying routing, standardizing layout for project creation and event triggers, and cleaning up the formatting and positioning of images.
Lint Fixes: Minor linting errors (such as extraneous braces in index.tsx) were corrected to ensure code consistency and avoid build issues.
Summary Table
Area | What Changed |
Docs & UI | “How It Works” modal with guide and screenshots |
Component Cleanup | Dialog close functionality fixed, redundant code removed |
Discord Docs | Documentation for Discord integration added |
Slack Docs | Documentation for Slack integration added |
Documentation Routing | Routing logic simplified, layout standardized, visuals aligned |
Code Quality | Linting issues resolved |
Solution Description For Issue #998
Our team resolved a bug where the socket notifier and run command error handling would display [object Object] instead of a meaningful error message. The root cause was improper formatting of the error object before logging it to the user. The fix ensures that error messages are now properly stringified or formatted, providing clearer and more helpful feedback during CLI failures. To verify the fix, you can run the keyshade run command with an invalid configuration or project setup that triggers a connection error. This update significantly improves the developer experience by making error outputs more understandable.
We determined that only three files required direct changes to address the issue:
NestJS WebSocket Gateway – The server-side component uses NestJS’s @WebSocketGateway decorator with Socket.IO to handle real-time communication between the CLI and API server. The handleRegister method in change-notifier.socket.ts processes client registration requests and can return different error message formats depending on the failure type (authentication, authorization, or database errors).
CLI Command Handler – The run.command.ts file contains the client-side logic that connects to the WebSocket server and handles the registration response. This component already included robust error parsing logic capable of handling both string and object formats, but the TypeScript interface was preventing proper type checking.
Type Checker – The run.types.d.ts file was updated to allow both string and object types, since the server can send either format.
After the maintainer approved the fix, we made additional updates to the codebase by modifying two more files: changelog.md to document the changes, and package.json to bump the semantic version to 3.2.3.
What is in the file changes
These were changes found using git’s diff command
What has been changed can also be found here
- changelog.md:
🐛 Bug Fixes* api, cli: ambiguous error messages on keyshade run failure ([#1087](https://github.com/keyshade-xyz/keyshade/issues/1087)) ([206dda7](https://github.com/keyshade-xyz/keyshade/commit/206dda702b3f3f0cabeb73e4f74dd2ccfc962631))
apps/cli/package.json: "version": "3.2.2-stage.2",
apps/cli/src/types/command/run.types.d.ts:
message: string | object // Allow both string and object types since server can send either
- apps/cli/src/commands/run.commands.ts:
Accurate WebSocket URL Parsing:
The CLI’s socket connection logic now uses the full host from the base URL (via the URL class), ensuring reliable and correct WebSocket connections especially for complex URLs.
Registration Timeout Added:A 30-second timeout was introduced for socket registration. If the server does not respond within this window, users receive a clear error message and the process exits gracefully. This prevents hanging connections and improves user feedback.
Enhanced Error Message Extraction:The registration response error handling is now more robust: It intelligently extracts meaningful error messages whether the server responds with a string, an object, or a nested structure. Attempts to parse JSON error details for more informative output. Falls back to stringifying unknown error types, ensuring users always get useful feedback. All error messages are now logged in a consistent, user-friendly format.
Minor Cleanup:Removed a redundant blank line after the socket connection initiation for improved code readability.
- apps/api/src/socket/change-notifier.socker.ts:
1. Improved Error Handling & Feedback The registration process now provides much more detailed error messages to clients. Instead of a generic error or raw message, errors are parsed, logged, and emitted in user-friendly formats.
2. Custom Authentication Logic for Socket Connections The previous decorator-based guards (@UseGuards(AuthGuard, ApiKeyGuard) and @RequiredApiKeyAuthorities) have been replaced by custom methods. New methods (extractAndValidateUser and convertToAuthenticatedUser) manually handle authentication and authority checks, mimicking guard behavior but allowing for more granular error handling and feedback.
3. API Key Authority Checks The code now checks if the API key has the required authorities (READ_WORKSPACE, READ_PROJECT, and READ_ENVIRONMENT) directly within the socket handler. If the user has ADMIN authority, individual checks are bypassed.
4. Enhanced Logging Connection logs now include the presence or absence of authentication headers. Errors during registration are logged with more context, including error types and constructors.
5. Dependency Injection Updates The SlugGenerator service is now injected into the class, preparing for potential future use or slug-related operations.
6. Cleaned-Up Imports Removed unused decorators and guards, reflecting the shift to manual authentication and authority verification.
7. User Context Construction The code now constructs a detailed user context object for socket connections, including IP address, workspace, and API key authorities, improving downstream authorization and personalization.
Testing the solution
While testing Keyshade, I encountered an issue that was easy to reproduce: create a project in Keyshade, configure your CLI to connect to it, delete the project, and then run the keyshade run <> command. This sequence would trigger an unclear error message, leaving the cause ambiguous. The expected behavior was for the CLI to clearly display the actual problem, with the API sending a properly formatted error message that the CLI would then print for the user. After applying the solution, the CLI behaved as intended, providing a clear and informative error message, making the debugging process much smoother.
Another testing approach involved building custom WebSocket test clients designed to send invalid authentication data, deliberately triggering error conditions to assess system resilience. We closely monitored server-side logs to ensure errors were correctly detected and handled, while also verifying that the client could parse various error message formats. This end-to-end validation—from CLI input, through WebSocket communication and API processing, to the final CLI response—confirmed the integrity and reliability of the entire flow. This comprehensive process not only confirmed the integrity and reliability of the entire system but also successfully closed issue #998 with our PR #1087.
Conclusion
We resolved a complex WebSocket bug by fixing authentication handling, type safety, and error messaging across the backend and CLI. They used systematic debugging, targeted fixes to three core components, and custom test clients to replace vague errors with clear feedback, improve authentication timeouts, and update integration docs—showcasing technical excellence, thorough testing, and a user-focused, collaborative approach.
Subscribe to my newsletter
Read articles from Johnny Santamaria directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Johnny Santamaria
Johnny Santamaria
Computer Science student at SFSU passionate about open source, web dev, and AI tools. Skilled in Python, JavaScript, and C++, I enjoy creating user-friendly solutions and teaching STEM topics.