diff --git a/app/controllers/api/v1/lists_controller.rb b/app/controllers/api/v1/lists_controller.rb
index 843ca2ec2..4bbbed267 100644
--- a/app/controllers/api/v1/lists_controller.rb
+++ b/app/controllers/api/v1/lists_controller.rb
@@ -42,6 +42,6 @@ class Api::V1::ListsController < Api::BaseController
   end
 
   def list_params
-    params.permit(:title, :replies_policy)
+    params.permit(:title, :replies_policy, :exclusive)
   end
 end
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index 2faa54b95..b0789cd42 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -151,10 +151,10 @@ export const createListFail = error => ({
   error,
 });
 
-export const updateList = (id, title, shouldReset, replies_policy) => (dispatch, getState) => {
+export const updateList = (id, title, shouldReset, isExclusive, replies_policy) => (dispatch, getState) => {
   dispatch(updateListRequest(id));
 
-  api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy }).then(({ data }) => {
+  api(getState).put(`/api/v1/lists/${id}`, { title, replies_policy, exclusive: typeof isExclusive === 'undefined' ? undefined : !!isExclusive }).then(({ data }) => {
     dispatch(updateListSuccess(data));
 
     if (shouldReset) {
diff --git a/app/javascript/mastodon/features/list_timeline/index.jsx b/app/javascript/mastodon/features/list_timeline/index.jsx
index f41e8e6f2..f9f3a7c31 100644
--- a/app/javascript/mastodon/features/list_timeline/index.jsx
+++ b/app/javascript/mastodon/features/list_timeline/index.jsx
@@ -8,6 +8,8 @@ import { Helmet } from 'react-helmet';
 import ImmutablePropTypes from 'react-immutable-proptypes';
 import { connect } from 'react-redux';
 
+import Toggle from 'react-toggle';
+
 import { addColumn, removeColumn, moveColumn } from 'mastodon/actions/columns';
 import { fetchList, deleteList, updateList } from 'mastodon/actions/lists';
 import { openModal } from 'mastodon/actions/modal';
@@ -145,7 +147,13 @@ class ListTimeline extends PureComponent {
   handleRepliesPolicyChange = ({ target }) => {
     const { dispatch } = this.props;
     const { id } = this.props.params;
-    dispatch(updateList(id, undefined, false, target.value));
+    dispatch(updateList(id, undefined, false, undefined, target.value));
+  };
+
+  onExclusiveToggle = ({ target }) => {
+    const { dispatch } = this.props;
+    const { id } = this.props.params;
+    dispatch(updateList(id, undefined, false, target.checked, undefined));
   };
 
   render () {
@@ -154,6 +162,7 @@ class ListTimeline extends PureComponent {
     const pinned = !!columnId;
     const title  = list ? list.get('title') : id;
     const replies_policy = list ? list.get('replies_policy') : undefined;
+    const isExclusive = list ? list.get('exclusive') : undefined;
 
     if (typeof list === 'undefined') {
       return (
@@ -191,6 +200,13 @@ class ListTimeline extends PureComponent {
             </button>
           </div>
 
+          <div className='setting-toggle'>
+            <Toggle id={`list-${id}-exclusive`} defaultChecked={isExclusive} onChange={this.onExclusiveToggle} />
+            <label htmlFor={`list-${id}-exclusive`} className='setting-toggle__label'>
+              <FormattedMessage id='lists.exclusive' defaultMessage='Hide these posts from home' />
+            </label>
+          </div>
+
           { replies_policy !== undefined && (
             <div role='group' aria-labelledby={`list-${id}-replies-policy`}>
               <span id={`list-${id}-replies-policy`} className='column-settings__section'>
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index f6d6daa3e..09282de7c 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -356,6 +356,7 @@
   "lists.delete": "Delete list",
   "lists.edit": "Edit list",
   "lists.edit.submit": "Change title",
+  "lists.exclusive": "Hide these posts from home",
   "lists.new.create": "Add list",
   "lists.new.title_placeholder": "New list title",
   "lists.replies_policy.followed": "Any followed user",
diff --git a/app/javascript/mastodon/reducers/list_editor.js b/app/javascript/mastodon/reducers/list_editor.js
index ceceb27c7..d3fd62ade 100644
--- a/app/javascript/mastodon/reducers/list_editor.js
+++ b/app/javascript/mastodon/reducers/list_editor.js
@@ -25,6 +25,7 @@ const initialState = ImmutableMap({
   isSubmitting: false,
   isChanged: false,
   title: '',
+  isExclusive: false,
 
   accounts: ImmutableMap({
     items: ImmutableList(),
@@ -46,6 +47,7 @@ export default function listEditorReducer(state = initialState, action) {
     return state.withMutations(map => {
       map.set('listId', action.list.get('id'));
       map.set('title', action.list.get('title'));
+      map.set('isExclusive', action.list.get('is_exclusive'));
       map.set('isSubmitting', false);
     });
   case LIST_EDITOR_TITLE_CHANGE:
diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb
index 643e6828d..7423d2d09 100644
--- a/app/lib/feed_manager.rb
+++ b/app/lib/feed_manager.rb
@@ -40,9 +40,9 @@ class FeedManager
   def filter?(timeline_type, status, receiver)
     case timeline_type
     when :home
-      filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]))
+      filter_from_home?(status, receiver.id, build_crutches(receiver.id, [status]), :home)
     when :list
-      filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]))
+      filter_from_list?(status, receiver) || filter_from_home?(status, receiver.account_id, build_crutches(receiver.account_id, [status]), :list)
     when :mentions
       filter_from_mentions?(status, receiver.id)
     when :tags
@@ -351,10 +351,11 @@ class FeedManager
   # @param [Integer] receiver_id
   # @param [Hash] crutches
   # @return [Boolean]
-  def filter_from_home?(status, receiver_id, crutches)
+  def filter_from_home?(status, receiver_id, crutches, timeline_type = :home)
     return false if receiver_id == status.account_id
     return true  if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?)
-    return true  if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
+    return true if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present?
+    return true if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language)
 
     check_for_blocks = crutches[:active_mentions][status.id] || []
     check_for_blocks.push(status.account_id)
@@ -543,13 +544,16 @@ class FeedManager
       arr
     end
 
-    crutches[:following]       = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
-    crutches[:languages]       = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
-    crutches[:hiding_reblogs]  = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
-    crutches[:blocking]        = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
-    crutches[:muting]          = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
-    crutches[:domain_blocking] = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
-    crutches[:blocked_by]      = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
+    lists = List.where(account_id: receiver_id, exclusive: true)
+
+    crutches[:following]            = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map(&:in_reply_to_account_id)).pluck(:target_account_id).index_with(true)
+    crutches[:languages]            = Follow.where(account_id: receiver_id, target_account_id: statuses.map(&:account_id)).pluck(:target_account_id, :languages).to_h
+    crutches[:hiding_reblogs]       = Follow.where(account_id: receiver_id, target_account_id: statuses.filter_map { |s| s.account_id if s.reblog? }, show_reblogs: false).pluck(:target_account_id).index_with(true)
+    crutches[:blocking]             = Block.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
+    crutches[:muting]               = Mute.where(account_id: receiver_id, target_account_id: check_for_blocks).pluck(:target_account_id).index_with(true)
+    crutches[:domain_blocking]      = AccountDomainBlock.where(account_id: receiver_id, domain: statuses.flat_map { |s| [s.account.domain, s.reblog&.account&.domain] }.compact).pluck(:domain).index_with(true)
+    crutches[:blocked_by]           = Block.where(target_account_id: receiver_id, account_id: statuses.map { |s| [s.account_id, s.reblog&.account_id] }.flatten.compact).pluck(:account_id).index_with(true)
+    crutches[:exclusive_list_users] = ListAccount.where(list: lists, account_id: statuses.map(&:account_id)).pluck(:account_id).index_with(true)
 
     crutches
   end
diff --git a/app/models/list.rb b/app/models/list.rb
index bd1bdbd24..7dc96f01b 100644
--- a/app/models/list.rb
+++ b/app/models/list.rb
@@ -10,6 +10,7 @@
 #  created_at     :datetime         not null
 #  updated_at     :datetime         not null
 #  replies_policy :integer          default("list"), not null
+#  exclusive      :boolean          default(FALSE)
 #
 
 class List < ApplicationRecord
diff --git a/app/serializers/rest/list_serializer.rb b/app/serializers/rest/list_serializer.rb
index 3e87f7119..6a1b6ea3e 100644
--- a/app/serializers/rest/list_serializer.rb
+++ b/app/serializers/rest/list_serializer.rb
@@ -1,7 +1,7 @@
 # frozen_string_literal: true
 
 class REST::ListSerializer < ActiveModel::Serializer
-  attributes :id, :title, :replies_policy
+  attributes :id, :title, :replies_policy, :exclusive
 
   def id
     object.id.to_s
diff --git a/db/migrate/20230605085710_add_exclusive_to_lists.rb b/db/migrate/20230605085710_add_exclusive_to_lists.rb
new file mode 100644
index 000000000..cc21a3e31
--- /dev/null
+++ b/db/migrate/20230605085710_add_exclusive_to_lists.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddExclusiveToLists < ActiveRecord::Migration[6.1]
+  def change
+    add_column :lists, :exclusive, :boolean, null: false, default: false
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 98fa5d600..35fbb8d2e 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2023_05_31_154811) do
+ActiveRecord::Schema.define(version: 2023_06_05_085710) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -567,6 +567,7 @@ ActiveRecord::Schema.define(version: 2023_05_31_154811) do
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
     t.integer "replies_policy", default: 0, null: false
+    t.boolean "exclusive", default: false
     t.index ["account_id"], name: "index_lists_on_account_id"
   end
 
diff --git a/spec/lib/feed_manager_spec.rb b/spec/lib/feed_manager_spec.rb
index 79d1f5249..31b53fd87 100644
--- a/spec/lib/feed_manager_spec.rb
+++ b/spec/lib/feed_manager_spec.rb
@@ -26,6 +26,7 @@ RSpec.describe FeedManager do
     let(:alice) { Fabricate(:account, username: 'alice') }
     let(:bob)   { Fabricate(:account, username: 'bob', domain: 'example.com') }
     let(:jeff)  { Fabricate(:account, username: 'jeff') }
+    let(:list) { Fabricate(:list, account: alice) }
 
     context 'with home feed' do
       it 'returns false for followee\'s status' do
@@ -153,6 +154,42 @@ RSpec.describe FeedManager do
         status = Fabricate(:status, text: 'Hallo Welt', account: bob, language: 'de')
         expect(FeedManager.instance.filter?(:home, status, alice)).to be false
       end
+
+      it 'returns true for post from followee on exclusive list' do
+        list.exclusive = true
+        alice.follow!(bob)
+        list.accounts << bob
+        allow(List).to receive(:where).and_return(list)
+        status = Fabricate(:status, text: 'I post a lot', account: bob)
+        expect(FeedManager.instance.filter?(:home, status, alice)).to be true
+      end
+
+      it 'returns true for reblog from followee on exclusive list' do
+        list.exclusive = true
+        alice.follow!(jeff)
+        list.accounts << jeff
+        allow(List).to receive(:where).and_return(list)
+        status = Fabricate(:status, text: 'I post a lot', account: bob)
+        reblog = Fabricate(:status, reblog: status, account: jeff)
+        expect(FeedManager.instance.filter?(:home, reblog, alice)).to be true
+      end
+
+      it 'returns false for post from followee on non-exclusive list' do
+        list.exclusive = false
+        alice.follow!(bob)
+        list.accounts << bob
+        status = Fabricate(:status, text: 'I post a lot', account: bob)
+        expect(FeedManager.instance.filter?(:home, status, alice)).to be false
+      end
+
+      it 'returns false for reblog from followee on non-exclusive list' do
+        list.exclusive = false
+        alice.follow!(jeff)
+        list.accounts << jeff
+        status = Fabricate(:status, text: 'I post a lot', account: bob)
+        reblog = Fabricate(:status, reblog: status, account: jeff)
+        expect(FeedManager.instance.filter?(:home, reblog, alice)).to be false
+      end
     end
 
     context 'with mentions feed' do