diff --git a/lib/permissions/minimally-encoded-thread-permissions.test.js b/lib/permissions/minimally-encoded-thread-permissions.test.js
--- a/lib/permissions/minimally-encoded-thread-permissions.test.js
+++ b/lib/permissions/minimally-encoded-thread-permissions.test.js
@@ -23,8 +23,10 @@
 import {
   minimallyEncodeRawThreadInfo,
   decodeMinimallyEncodedRawThreadInfo,
+  minimallyEncodeThreadCurrentUserInfo,
 } from '../types/minimally-encoded-thread-permissions-types.js';
 import type { ThreadRolePermissionsBlob } from '../types/thread-permission-types.js';
+import type { ThreadCurrentUserInfo } from '../types/thread-types.js';
 
 const permissions = {
   know_of: { value: true, source: '1' },
@@ -480,3 +482,127 @@
     ).toStrictEqual(expectedDecodedExampleRawThreadInfoA);
   });
 });
+
+const threadCurrentUserInfo: ThreadCurrentUserInfo = {
+  role: '256|83795',
+  permissions: {
+    know_of: {
+      value: true,
+      source: '256|1',
+    },
+    visible: {
+      value: true,
+      source: '256|1',
+    },
+    voiced: {
+      value: false,
+      source: null,
+    },
+    edit_entries: {
+      value: false,
+      source: null,
+    },
+    edit_thread: {
+      value: false,
+      source: null,
+    },
+    edit_thread_description: {
+      value: false,
+      source: null,
+    },
+    edit_thread_color: {
+      value: false,
+      source: null,
+    },
+    delete_thread: {
+      value: false,
+      source: null,
+    },
+    create_subthreads: {
+      value: false,
+      source: null,
+    },
+    create_sidebars: {
+      value: false,
+      source: null,
+    },
+    join_thread: {
+      value: false,
+      source: null,
+    },
+    edit_permissions: {
+      value: false,
+      source: null,
+    },
+    add_members: {
+      value: false,
+      source: null,
+    },
+    remove_members: {
+      value: false,
+      source: null,
+    },
+    change_role: {
+      value: false,
+      source: null,
+    },
+    leave_thread: {
+      value: false,
+      source: null,
+    },
+    react_to_message: {
+      value: false,
+      source: null,
+    },
+    edit_message: {
+      value: false,
+      source: null,
+    },
+    edit_thread_avatar: {
+      value: false,
+      source: null,
+    },
+    manage_pins: {
+      value: false,
+      source: null,
+    },
+    manage_invite_links: {
+      value: false,
+      source: null,
+    },
+    voiced_in_announcement_channels: {
+      value: false,
+      source: null,
+    },
+  },
+  subscription: {
+    home: true,
+    pushNotifs: true,
+  },
+  unread: true,
+};
+describe('minimallyEncodeThreadCurrentUserInfo', () => {
+  it('should correctly encode threadCurrentUserInfo ONCE', () => {
+    const minimallyEncoded = minimallyEncodeThreadCurrentUserInfo(
+      threadCurrentUserInfo,
+    );
+    expect(minimallyEncoded.permissions).toBe('3');
+  });
+
+  it('should throw when attempting to minimally encode threadCurrentUserInfo twice', () => {
+    const minimallyEncoded = minimallyEncodeThreadCurrentUserInfo(
+      threadCurrentUserInfo,
+    );
+    expect(minimallyEncoded.permissions).toBe('3');
+    expect(() =>
+      // `MinimallyEncodedThreadCurrentUser` should never be passed
+      // to `minimallyEncodeThreadCurrentUserInfo`. We're intentionally
+      // bypassing Flow to simulate a scenario where malformed input is
+      // passed to minimallyEncodeThreadCurrentUserInfo to ensure that the
+      // `invariant` throws the expected error.
+
+      // $FlowExpectedError
+      minimallyEncodeThreadCurrentUserInfo(minimallyEncoded),
+    ).toThrow('threadCurrentUserInfo is already minimally encoded.');
+  });
+});
diff --git a/lib/types/minimally-encoded-thread-permissions-types.js b/lib/types/minimally-encoded-thread-permissions-types.js
--- a/lib/types/minimally-encoded-thread-permissions-types.js
+++ b/lib/types/minimally-encoded-thread-permissions-types.js
@@ -1,5 +1,6 @@
 // @flow
 
+import invariant from 'invariant';
 import _mapValues from 'lodash/fp/mapValues.js';
 
 import type {
@@ -51,11 +52,17 @@
 
 const minimallyEncodeThreadCurrentUserInfo = (
   threadCurrentUserInfo: ThreadCurrentUserInfo,
-): MinimallyEncodedThreadCurrentUserInfo => ({
-  ...threadCurrentUserInfo,
-  minimallyEncoded: true,
-  permissions: permissionsToBitmaskHex(threadCurrentUserInfo.permissions),
-});
+): MinimallyEncodedThreadCurrentUserInfo => {
+  invariant(
+    !('minimallyEncoded' in threadCurrentUserInfo),
+    'threadCurrentUserInfo is already minimally encoded.',
+  );
+  return {
+    ...threadCurrentUserInfo,
+    minimallyEncoded: true,
+    permissions: permissionsToBitmaskHex(threadCurrentUserInfo.permissions),
+  };
+};
 
 const decodeMinimallyEncodedThreadCurrentUserInfo = (
   minimallyEncodedThreadCurrentUserInfo: MinimallyEncodedThreadCurrentUserInfo,