Summary
API keys and other sensitive credentials are currently displayed in plain text in the Web UI's provider settings screens. This applies to:
- Custom Models modal — The API key input field uses
type="text", and the backend GET endpoints return the full API key in JSON responses.
- Notification Channels modal — Sensitive config fields like Discord webhook URLs are similarly displayed as plain text inputs.
This means API keys are visible on screen, in browser DevTools network logs, and in any client-side state.
Current Behavior
ModelsModal.tsx renders the API key in a type="text" input (line 391)
- The
GET /api/custom-models endpoint returns the full api_key value in the JSON response (toModelAPI in custom_models.go)
- When editing an existing model, the full key is loaded from the API response into the form
- Discord webhook URLs in
NotificationsModal.tsx are rendered as regular text inputs
Suggested Approach
Here's one possible direction — happy to discuss alternatives:
Backend
- Mask API keys in GET responses: Modify
toModelAPI() to return a masked version of the key (e.g. sk-a••••xZ9f — first 4 + last 4 characters). This prevents the full key from ever reaching the client on read operations.
- Preserve existing keys on update: The update handler already supports keeping the existing key when
api_key is empty. This could be extended to also detect when the masked value is sent back unchanged.
- Notification channels: Add a
sensitive flag to ConfigField and apply the same masking pattern to sensitive config values (e.g. Discord webhook URLs). The update handler would need to merge sensitive fields so that masked values sent back from the client are replaced with the stored originals.
Frontend
- Use
type="password" for sensitive inputs: Switch the API key input to type="password". When editing, start with the field empty and use a placeholder like "Leave blank to keep current key".
- Relax edit-mode validation: Currently
handleSave requires api_key even when editing. This could be changed to only require it for new models, since the backend preserves the existing key when the field is empty.
- Notification channels: Use the
sensitive flag from ConfigField metadata to dynamically render sensitive fields as password inputs with the same empty-on-edit pattern.
Existing patterns that help
handleUpdateModel already preserves the existing key when api_key is empty (line 216-220 of custom_models.go)
handleTestModel already accepts model_id to look up the stored key server-side, so testing works without sending the key from the client
handleDuplicateModel copies the key server-side — the frontend never sends it
Affected Files
server/custom_models.go
server/notification_channels.go
ui/src/components/ModelsModal.tsx
ui/src/components/NotificationsModal.tsx
ui/src/services/api.ts
Thank you for building Shelley.
Summary
API keys and other sensitive credentials are currently displayed in plain text in the Web UI's provider settings screens. This applies to:
type="text", and the backend GET endpoints return the full API key in JSON responses.This means API keys are visible on screen, in browser DevTools network logs, and in any client-side state.
Current Behavior
ModelsModal.tsxrenders the API key in atype="text"input (line 391)GET /api/custom-modelsendpoint returns the fullapi_keyvalue in the JSON response (toModelAPIincustom_models.go)NotificationsModal.tsxare rendered as regular text inputsSuggested Approach
Here's one possible direction — happy to discuss alternatives:
Backend
toModelAPI()to return a masked version of the key (e.g.sk-a••••xZ9f— first 4 + last 4 characters). This prevents the full key from ever reaching the client on read operations.api_keyis empty. This could be extended to also detect when the masked value is sent back unchanged.sensitiveflag toConfigFieldand apply the same masking pattern to sensitive config values (e.g. Discord webhook URLs). The update handler would need to merge sensitive fields so that masked values sent back from the client are replaced with the stored originals.Frontend
type="password"for sensitive inputs: Switch the API key input totype="password". When editing, start with the field empty and use a placeholder like "Leave blank to keep current key".handleSaverequiresapi_keyeven when editing. This could be changed to only require it for new models, since the backend preserves the existing key when the field is empty.sensitiveflag fromConfigFieldmetadata to dynamically render sensitive fields as password inputs with the same empty-on-edit pattern.Existing patterns that help
handleUpdateModelalready preserves the existing key whenapi_keyis empty (line 216-220 ofcustom_models.go)handleTestModelalready acceptsmodel_idto look up the stored key server-side, so testing works without sending the key from the clienthandleDuplicateModelcopies the key server-side — the frontend never sends itAffected Files
server/custom_models.goserver/notification_channels.goui/src/components/ModelsModal.tsxui/src/components/NotificationsModal.tsxui/src/services/api.tsThank you for building Shelley.