diff --git a/libs/client/src/person.rs b/libs/client/src/person.rs
index a5fdbf71b..5734ac174 100644
--- a/libs/client/src/person.rs
+++ b/libs/client/src/person.rs
@@ -6,6 +6,10 @@ use kanidm_proto::v1::{AccountUnixExtend, Entry, SingleStringRequest, UatStatus}
 use uuid::Uuid;
 
 use crate::{ClientError, KanidmClient};
+use kanidm_proto::scim_v1::PatchRequest;
+use serde_json::Value;
+
+use tracing::trace;
 
 impl KanidmClient {
     pub async fn idm_person_account_list(&self) -> Result<Vec<Entry>, ClientError> {
@@ -286,4 +290,16 @@ impl KanidmClient {
         self.perform_post_request(format!("/v1/person/{}/_certificate", id).as_str(), new_cert)
             .await
     }
+
+    pub async fn idm_person_account_patch(
+        &self,
+        id: &str,
+        patch_request: PatchRequest,
+    ) -> Result<(), ClientError> {
+        if patch_request.operations.is_empty() {
+            return Ok(());
+        }
+        self.perform_patch_request_scim(format!("/v1/person/{}", id).as_str(), patch_request)
+            .await
+    }
 }
diff --git a/tools/cli/src/cli/person.rs b/tools/cli/src/cli/person.rs
index 87c2ad765..1ec2fb358 100644
--- a/tools/cli/src/cli/person.rs
+++ b/tools/cli/src/cli/person.rs
@@ -30,6 +30,36 @@ use crate::{
     AccountSsh, AccountUserAuthToken, AccountValidity, OutputMode, PersonOpt, PersonPosix,
 };
 
+use kanidm_client::ClientError;
+use kanidm_client::OutputFormat;
+use kanidm_client::SSHKEY_ATTEST_FEATURE;
+use kanidm_proto::identity::v1::{
+    IdentityGetReply, KeyAttestationVerifyRequest, KeyType as SSHKeyAttestationType,
+};
+use kanidm_proto::identity::{self};
+use kanidm_proto::internal::{
+    CUCredState, CURegState, CURegWarning, CURequest, CUSessionToken, CUStatus, CredentialDetail,
+    PasskeyDetail, SshPublicKey, TotpSecret,
+};
+use kanidm_proto::scim_v1::{
+    client::{ScimSshPublicKeys, PATCHOP_SCHEMA_URI}, 
+    PatchOp, PatchOperation, PatchRequest, ScimEntryGetQuery,
+    ATTR_DISPLAYNAME, ATTR_GIDNUMBER, ATTR_LEGALNAME, ATTR_MAIL, ATTR_NAME, ATTR_PASSWORD, ATTR_POSIXACCOUNTS,
+    ATTR_SSH_PUBLICKEY,
+};
+use kanidm_proto::MessageError;
+use serde_json::json;
+use sshkey_attest::{Error as AttestationError, RegisterChallengeResponse};
+use sshkeys::PublicKey as ActualSshPublicKey;
+use std::collections::BTreeMap;
+use std::fmt;
+use std::path::PathBuf;
+use std::str::FromStr;
+
+use dialoguer::Input;
+use tracing::{debug, error, info, trace, warn};
+use uuid::Uuid;
+
 impl PersonOpt {
     pub fn debug(&self) -> bool {
         match self {
@@ -321,14 +351,71 @@ impl PersonOpt {
             }
             PersonOpt::Update(aopt) => {
                 let client = aopt.copt.to_client(OpType::Write).await;
+
+                let mut patch_ops = Vec::new();
+
+                // Helper to create a replace operation
+                let replace_op = |path: &str, value: &str| -> PatchOperation {
+                    PatchOperation {
+                        op: PatchOp::Replace,
+                        path: Some(path.to_string()),
+                        value: Some(json!(value)),
+                    }
+                };
+
+                // Helper to create a replace operation for Vec<String>
+                let replace_op_vec = |path: &str, value: &[String]| -> PatchOperation {
+                    PatchOperation {
+                        op: PatchOp::Replace,
+                        path: Some(path.to_string()),
+                        value: Some(json!(value)),
+                    }
+                };
+
+                // Helper to create a remove operation
+                let remove_op = |path: &str| -> PatchOperation {
+                    PatchOperation {
+                        op: PatchOp::Remove,
+                        path: Some(path.to_string()),
+                        value: None,
+                    }
+                };
+
+                if let Some(newname) = &aopt.newname {
+                    patch_ops.push(replace_op(ATTR_NAME, newname));
+                }
+                if let Some(displayname) = &aopt.displayname {
+                    patch_ops.push(replace_op(ATTR_DISPLAYNAME, displayname));
+                }
+                if let Some(legalname) = &aopt.legalname {
+                    if legalname.is_empty() {
+                        // Empty string means clear the attribute
+                        patch_ops.push(remove_op(ATTR_LEGALNAME));
+                    } else {
+                        patch_ops.push(replace_op(ATTR_LEGALNAME, legalname));
+                    }
+                }
+                if let Some(mail) = &aopt.mail {
+                    if mail.is_empty() {
+                         // Empty list means clear the attribute
+                        patch_ops.push(remove_op(ATTR_MAIL));
+                    } else {
+                        patch_ops.push(replace_op_vec(ATTR_MAIL, mail));
+                    }
+                }
+
+                if patch_ops.is_empty() {
+                    println!("No changes specified.");
+                    return;
+                }
+
+                let patch_request = PatchRequest {
+                    schemas: vec![PATCHOP_SCHEMA_URI.to_string()],
+                    operations: patch_ops,
+                };
+
                 match client
-                    .idm_person_account_update(
-                        aopt.aopts.account_id.as_str(),
-                        aopt.newname.as_deref(),
-                        aopt.displayname.as_deref(),
-                        aopt.legalname.as_deref(),
-                        aopt.mail.as_deref(),
-                    )
+                    .idm_person_account_patch(aopt.aopts.account_id.as_str(), patch_request)
                     .await
                 {
                     Ok(()) => println!("Success"),
diff --git a/tools/cli/src/opt/kanidm.rs b/tools/cli/src/opt/kanidm.rs
index 5e02a10ba..9b197504e 100644
--- a/tools/cli/src/opt/kanidm.rs
+++ b/tools/cli/src/opt/kanidm.rs
@@ -594,8 +594,7 @@ pub enum ServiceAccountPosix {
 pub struct PersonUpdateOpt {
     #[clap(flatten)]
     aopts: AccountCommonOpt,
-    #[clap(long, short, help = "Set the legal name for the person.",
-    value_parser = clap::builder::NonEmptyStringValueParser::new())]
+    #[clap(long, short, help = "Set the legal name for the person. Use '' to clear.")]
     legalname: Option<String>,
     #[clap(long, short, help = "Set the account name for the person.",
     value_parser = clap::builder::NonEmptyStringValueParser::new())]