gitbutlerapp/gitbutler · error
BUG: Sensitive data cannot be serialized - it needs to be ex
Error message
BUG: Sensitive data cannot be serialized - it needs to be extracted and put into a struct for serialization explicitly
What it means
`Sensitive<T>` in but-secret is a secrecy wrapper whose blanket serde `Serialize` impl deliberately panics: serializing secrets accidentally (state files, logs, IPC payloads, API DTOs) is treated as a bug. To persist or transmit data containing a `Sensitive` field you must build an explicit serializable struct and consciously include (or omit) the secret (crates/but-secret/src/sensitive.rs:15).
Source
Thrown at crates/but-secret/src/sensitive.rs:15
use std::ops::{Deref, DerefMut};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::Sensitive;
impl<T> Serialize for Sensitive<T>
where
T: Serialize,
{
fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
unreachable!(
"BUG: Sensitive data cannot be serialized - it needs to be extracted and put into a struct for serialization explicitly"
)
}
}
impl<'de, T> Deserialize<'de> for Sensitive<T>
where
T: Deserialize<'de>,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
T::deserialize(deserializer).map(Sensitive)
}
}
impl<T> std::fmt::Debug for Sensitive<T>
whereView on GitHub (pinned to caf1f223d3)
Solutions
- Introduce a dedicated serializable DTO that omits the secret or exposes it explicitly via the wrapper's expose API, and convert before serializing
- If the field must stay on the serialized type, exclude it with `#[serde(skip)]` and default-construct it on load
- Audit with a grep for `Sensitive<` inside `#[derive(Serialize)]` types as a review check
Example fix
// before
#[derive(serde::Serialize)]
struct Session {
name: String,
token: but_secret::Sensitive<String>, // panics at to_string()
}
serde_json::to_string(&session)?;
// after - explicit DTO, secret consciously handled
#[derive(serde::Serialize)]
struct SessionDto {
name: String,
// token intentionally omitted; never serialized
}
let dto = SessionDto { name: session.name.clone() };
serde_json::to_string(&dto)?; Defensive patterns
Strategy: type-guard
Validate before calling
// Keep Sensitive fields out of serialized types by construction:
// define DTOs explicitly and convert before serde ever sees the domain type
#[derive(serde::Serialize)]
struct AuthDto {
username: String,
// no token field: Sensitive<String> stays in the domain type only
} Type guard
// Compile-time guard: registering a domain type for serialization fails if it (still)
// contains Sensitive, because Sensitive's Serialize impl panics at runtime - so instead
// whitelist explicit DTOs:
trait SafeSerialize: serde::Serialize {}
impl SafeSerialize for AuthDto {} // only DTOs opt in; domain types never implement it Try / catch
// If a panic escapes third-party code:
let json = std::panic::catch_unwind(|| serde_json::to_string(&value))
.map_err(|_| anyhow::anyhow!("type contained Sensitive data; build an explicit DTO"))?; Prevention
- Never #[derive(Serialize)] on structs containing Sensitive<T>
- Convert domain types to DTOs at API/IPC boundaries before serialization
- Use #[serde(skip)] if a Sensitive field must ride along on an otherwise-serializable struct
- Grep the codebase for 'Sensitive<' inside files with 'derive(Serialize)' as a review gate
When it happens
Trigger: `#[derive(Serialize)]` on any struct that contains a `Sensitive<T>` field, followed by serde_json/toml serialization or returning it from a Tauri command; recursively serializing a domain type that embeds Sensitive.
Common situations: Adding a new config/state/session struct that carries a token or credential; passing internal domain types across an API boundary instead of a DTO; snapshotting or logging app state for debugging.
Related errors
- unused
- symbols must always be initialized
- broker already configured
- broker has not been configured
- BUG: {id} is queued
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/d56b2606e1e94d41.
Report an issue: GitHub.