diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 2753ec97ff12795a0718832e7c15d48ff11d9cc0..8cab61e0017bcc54c095fbb497b23e56f1b066ef 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -6,6 +6,7 @@
 
 ## Features
 - cta/CTA#999 - Add a default mount rule for recalls
+- cta/CTA#1109 - Add --dirtybit option to cta-admin ta ch and show dirty bit value in cta-admin --json ta ls
 
 ## Bug fixes
 - cta/CTA#1102 - Make requeued jobs retain their original creation time
diff --git a/catalogue/Catalogue.hpp b/catalogue/Catalogue.hpp
index af2358e3038fcd187e4484be79895684f98f42c7..70225de03f93bc838117db1a5815bf5fd7acaca3 100644
--- a/catalogue/Catalogue.hpp
+++ b/catalogue/Catalogue.hpp
@@ -692,6 +692,18 @@ public:
    */
   virtual void setTapeFull(const common::dataStructures::SecurityIdentity &admin, const std::string &vid, const bool fullValue) = 0;
 
+  /**
+   * Sets the dirty status of the specified tape.
+   *
+   * Please note that this method is to be called by the CTA front-end in
+   * response to a command from the CTA command-line interface (CLI).
+   *
+   * @param admin The administrator.
+   * @param vid The volume identifier of the tape to be marked as full.
+   * @param dirty Set to true if the tape is dirty.
+   */
+  virtual void setTapeDirty(const common::dataStructures::SecurityIdentity &admin, const std::string &vid, const bool dirty) = 0;
+
   /**
    * This method notifies the CTA catalogue to set the specified tape is from CASTOR.
    * This method only for unitTests and MUST never be called in CTA!!!
diff --git a/catalogue/CatalogueRetryWrapper.hpp b/catalogue/CatalogueRetryWrapper.hpp
index 393de1e8930502232f5d54a5cc2e896bad25c079..814eef158a4406aba0dc08fcc1aa5b7c60dc83d9 100644
--- a/catalogue/CatalogueRetryWrapper.hpp
+++ b/catalogue/CatalogueRetryWrapper.hpp
@@ -388,6 +388,10 @@ public:
     return retryOnLostConnection(m_log, [&]{return m_catalogue->setTapeFull(admin, vid, fullValue);}, m_maxTriesToConnect);
   }
 
+  void setTapeDirty(const common::dataStructures::SecurityIdentity &admin, const std::string &vid, const bool dirtyValue) override {
+    return retryOnLostConnection(m_log, [&]{return m_catalogue->setTapeDirty(admin, vid, dirtyValue);}, m_maxTriesToConnect);
+  }
+
   void setTapeIsFromCastorInUnitTests(const std::string &vid) override {
     return retryOnLostConnection(m_log, [&]{return m_catalogue->setTapeIsFromCastorInUnitTests(vid);}, m_maxTriesToConnect);
   }
diff --git a/catalogue/CatalogueTest.cpp b/catalogue/CatalogueTest.cpp
index 605f56bb9fd75278fb742b10c35408fdebe542b3..d5d5eb922eff6ba371f3952c220df164071a5778 100644
--- a/catalogue/CatalogueTest.cpp
+++ b/catalogue/CatalogueTest.cpp
@@ -6574,6 +6574,88 @@ TEST_P(cta_catalogue_CatalogueTest, setTapeFull_nonExistentTape) {
   ASSERT_THROW(m_catalogue->setTapeFull(m_admin, m_tape1.vid, true), exception::UserError);
 }
 
+TEST_P(cta_catalogue_CatalogueTest, setTapeDirty) {
+  using namespace cta;
+
+  const bool logicalLibraryIsDisabled= false;
+  const uint64_t nbPartialTapes = 2;
+  const bool isEncrypted = true;
+  const cta::optional<std::string> supply("value for the supply pool mechanism");
+
+  m_catalogue->createMediaType(m_admin, m_mediaType);
+  m_catalogue->createLogicalLibrary(m_admin, m_tape1.logicalLibraryName, logicalLibraryIsDisabled, "Create logical library");
+
+  m_catalogue->createVirtualOrganization(m_admin, m_vo);
+  m_catalogue->createTapePool(m_admin, m_tape1.tapePoolName, m_vo.name, nbPartialTapes, isEncrypted, supply, "Create tape pool");
+
+  m_catalogue->createTape(m_admin, m_tape1);
+
+  {
+    const std::list<common::dataStructures::Tape> tapes = m_catalogue->getTapes();
+
+    ASSERT_EQ(1, tapes.size());
+
+    const common::dataStructures::Tape tape = tapes.front();
+    ASSERT_EQ(m_tape1.vid, tape.vid);
+    ASSERT_EQ(m_tape1.mediaType, tape.mediaType);
+    ASSERT_EQ(m_tape1.vendor, tape.vendor);
+    ASSERT_EQ(m_tape1.logicalLibraryName, tape.logicalLibraryName);
+    ASSERT_EQ(m_tape1.tapePoolName, tape.tapePoolName);
+    ASSERT_EQ(m_vo.name, tape.vo);
+    ASSERT_EQ(m_mediaType.capacityInBytes, tape.capacityInBytes);
+    ASSERT_EQ(m_tape1.full, tape.full);
+    ASSERT_TRUE(tape.dirty);
+
+    ASSERT_FALSE(tape.isFromCastor);
+    ASSERT_EQ(m_tape1.comment, tape.comment);
+    ASSERT_FALSE(tape.labelLog);
+    ASSERT_FALSE(tape.lastReadLog);
+    ASSERT_FALSE(tape.lastWriteLog);
+
+    const common::dataStructures::EntryLog creationLog = tape.creationLog;
+    ASSERT_EQ(m_admin.username, creationLog.username);
+    ASSERT_EQ(m_admin.host, creationLog.host);
+
+    const common::dataStructures::EntryLog lastModificationLog = tape.lastModificationLog;
+    ASSERT_EQ(creationLog, lastModificationLog);
+  }
+
+  m_catalogue->setTapeDirty(m_admin, m_tape1.vid, false);
+
+  {
+    const std::list<common::dataStructures::Tape> tapes = m_catalogue->getTapes();
+
+    ASSERT_EQ(1, tapes.size());
+
+    const common::dataStructures::Tape tape = tapes.front();
+    ASSERT_EQ(m_tape1.vid, tape.vid);
+    ASSERT_EQ(m_tape1.mediaType, tape.mediaType);
+    ASSERT_EQ(m_tape1.vendor, tape.vendor);
+    ASSERT_EQ(m_tape1.logicalLibraryName, tape.logicalLibraryName);
+    ASSERT_EQ(m_tape1.tapePoolName, tape.tapePoolName);
+    ASSERT_EQ(m_vo.name, tape.vo);
+    ASSERT_EQ(m_mediaType.capacityInBytes, tape.capacityInBytes);
+    ASSERT_EQ(m_tape1.full, tape.full);
+    ASSERT_FALSE(tape.dirty);
+
+    ASSERT_FALSE(tape.isFromCastor);
+    ASSERT_EQ(m_tape1.comment, tape.comment);
+    ASSERT_FALSE(tape.labelLog);
+    ASSERT_FALSE(tape.lastReadLog);
+    ASSERT_FALSE(tape.lastWriteLog);
+
+    const common::dataStructures::EntryLog creationLog = tape.creationLog;
+    ASSERT_EQ(m_admin.username, creationLog.username);
+    ASSERT_EQ(m_admin.host, creationLog.host);
+  }
+}
+
+TEST_P(cta_catalogue_CatalogueTest, setTapeDirty_nonExistentTape) {
+  using namespace cta;
+
+  ASSERT_THROW(m_catalogue->setTapeDirty(m_admin, m_tape1.vid, true), exception::UserError);
+}
+
 TEST_P(cta_catalogue_CatalogueTest, noSpaceLeftOnTape) {
   using namespace cta;
 
diff --git a/catalogue/DummyCatalogue.hpp b/catalogue/DummyCatalogue.hpp
index 5837e17a2a33980577f814f2d230d910bb41b915..b752db52981924002de960097f99ee27af950c85 100644
--- a/catalogue/DummyCatalogue.hpp
+++ b/catalogue/DummyCatalogue.hpp
@@ -170,6 +170,7 @@ public:
   uint64_t getNbFilesOnTape(const std::string& vid) const  override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   void setTapeDisabled(const common::dataStructures::SecurityIdentity& admin, const std::string& vid, const std::string & reason) override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   void setTapeFull(const common::dataStructures::SecurityIdentity& admin, const std::string& vid, const bool fullValue) override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
+  void setTapeDirty(const common::dataStructures::SecurityIdentity& admin, const std::string& vid, const bool dirtyValue) override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   void setTapeDirty(const std::string & vid) override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   void setTapeIsFromCastorInUnitTests(const std::string &vid) override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
   void setTapePoolEncryption(const common::dataStructures::SecurityIdentity& admin, const std::string& name, const bool encryptionValue) override { throw exception::Exception(std::string("In ")+__PRETTY_FUNCTION__+": not implemented"); }
diff --git a/catalogue/RdbmsCatalogue.cpp b/catalogue/RdbmsCatalogue.cpp
index d3b485922ea0b2a5846ccbe8f1409c76c8fb6d9d..adfda2ac35ff141d819134279f8e9f24b9be53d9 100644
--- a/catalogue/RdbmsCatalogue.cpp
+++ b/catalogue/RdbmsCatalogue.cpp
@@ -3869,6 +3869,8 @@ std::list<common::dataStructures::Tape> RdbmsCatalogue::getTapes(rdbms::Conn &co
         "TAPE.MASTER_DATA_IN_BYTES AS MASTER_DATA_IN_BYTES,"
         "TAPE.LAST_FSEQ AS LAST_FSEQ,"
         "TAPE.IS_FULL AS IS_FULL,"
+        "TAPE.DIRTY AS DIRTY,"
+        
         "TAPE.IS_FROM_CASTOR AS IS_FROM_CASTOR,"
 
         "TAPE.LABEL_DRIVE AS LABEL_DRIVE,"
@@ -4039,6 +4041,7 @@ std::list<common::dataStructures::Tape> RdbmsCatalogue::getTapes(rdbms::Conn &co
         tape.masterDataInBytes = rset.columnUint64("MASTER_DATA_IN_BYTES");
         tape.lastFSeq = rset.columnUint64("LAST_FSEQ");
         tape.full = rset.columnBool("IS_FULL");
+        tape.dirty = rset.columnBool("DIRTY");
         tape.isFromCastor = rset.columnBool("IS_FROM_CASTOR");
 
         tape.labelLog = getTapeLogFromRset(rset, "LABEL_DRIVE", "LABEL_TIME");
@@ -5023,6 +5026,51 @@ void RdbmsCatalogue::setTapeFull(const common::dataStructures::SecurityIdentity
   }
 }
 
+//------------------------------------------------------------------------------
+// setTapeDirty
+//------------------------------------------------------------------------------
+void RdbmsCatalogue::setTapeDirty(const common::dataStructures::SecurityIdentity &admin, const std::string &vid,
+  const bool dirtyValue) {
+  try {
+    const time_t now = time(nullptr);
+    const char *const sql =
+      "UPDATE TAPE SET "
+        "DIRTY = :DIRTY,"
+        "LAST_UPDATE_USER_NAME = :LAST_UPDATE_USER_NAME,"
+        "LAST_UPDATE_HOST_NAME = :LAST_UPDATE_HOST_NAME,"
+        "LAST_UPDATE_TIME = :LAST_UPDATE_TIME "
+      "WHERE "
+        "VID = :VID";
+    auto conn = m_connPool.getConn();
+    auto stmt = conn.createStmt(sql);
+    stmt.bindBool(":DIRTY", dirtyValue);
+    stmt.bindString(":LAST_UPDATE_USER_NAME", admin.username);
+    stmt.bindString(":LAST_UPDATE_HOST_NAME", admin.host);
+    stmt.bindUint64(":LAST_UPDATE_TIME", now);
+    stmt.bindString(":VID", vid);
+    stmt.executeNonQuery();
+
+    if(0 == stmt.getNbAffectedRows()) {
+      throw exception::UserError(std::string("Cannot modify tape ") + vid + " because it does not exist");
+    }
+
+    log::LogContext lc(m_log);
+    log::ScopedParamContainer spc(lc);
+    spc.add("vid", vid)
+       .add("dirty", dirtyValue ? 1 : 0)
+       .add("lastUpdateUserName", admin.username)
+       .add("lastUpdateHostName", admin.host)
+       .add("lastUpdateTime", now);
+    lc.log(log::INFO, "Catalogue - user modified tape - dirty");
+  } catch(exception::UserError &) {
+    throw;
+  } catch(exception::Exception &ex) {
+    ex.getMessage().str(std::string(__FUNCTION__) + ": " + ex.getMessage().str());
+    throw;
+  }
+}
+
+
 //------------------------------------------------------------------------------
 // noSpaceLeftOnTape
 //------------------------------------------------------------------------------
diff --git a/catalogue/RdbmsCatalogue.hpp b/catalogue/RdbmsCatalogue.hpp
index 2cce21484d92f7a13eed0257e8f84e6cc511f2fc..b40e52802f171c0f5a723eb956316eea4e63253f 100644
--- a/catalogue/RdbmsCatalogue.hpp
+++ b/catalogue/RdbmsCatalogue.hpp
@@ -645,6 +645,18 @@ public:
    */
   void setTapeFull(const common::dataStructures::SecurityIdentity &admin, const std::string &vid, const bool fullValue) override;
 
+  /**
+   * Sets the dirty status of the specified tape.
+   *
+   * Please note that this method is to be called by the CTA front-end in
+   * response to a command from the CTA command-line interface (CLI).
+   *
+   * @param admin The administrator.
+   * @param vid The volume identifier of the tape to be marked as full.
+   * @param dirtyValue Set to true if the tape is dirty.
+   */
+  void setTapeDirty(const common::dataStructures::SecurityIdentity &admin, const std::string &vid, const bool dirtyValue) override;
+
   /**
    * This method notifies the CTA catalogue to set the specified tape is from CASTOR.
    * This method only for unitTests and MUST never be called in CTA!!!
diff --git a/cmdline/CtaAdminCmdParse.hpp b/cmdline/CtaAdminCmdParse.hpp
index f7be0cd0f2652380d90dc0120ad9a2249ebe52e6..aee76b660db3ce2da2367cd18515704834742685 100644
--- a/cmdline/CtaAdminCmdParse.hpp
+++ b/cmdline/CtaAdminCmdParse.hpp
@@ -277,7 +277,8 @@ const std::map<std::string, OptionBoolean::Key> boolOptions = {
    { "--log",                   OptionBoolean::SHOW_LOG_ENTRIES },
    { "--lookupnamespace",       OptionBoolean::LOOKUP_NAMESPACE },
    { "--summary",               OptionBoolean::SUMMARY },
-   { "--no-recall",             OptionBoolean::NO_RECALL }
+   { "--no-recall",             OptionBoolean::NO_RECALL },
+   { "--dirtybit",             OptionBoolean::DIRTY_BIT }
 };
 
 
@@ -460,6 +461,7 @@ const Option opt_filename             { Option::OPT_STR,  "--file",
 const Option opt_force                { Option::OPT_BOOL, "--force",                 "-f",   " <\"true\" or \"false\">" };
 const Option opt_force_flag           { Option::OPT_FLAG, "--force",                 "-f",   "" };
 const Option opt_fromcastor           { Option::OPT_BOOL, "--fromcastor",            "--fc",  " <\"true\" or \"false\">"};      
+const Option opt_dirtybit             { Option::OPT_BOOL, "--dirtybit",              "--db",  " <\"true\" or \"false\">"};      
 const Option opt_instance             { Option::OPT_STR,  "--instance",              "-i",   " <disk_instance>" };
 const Option opt_justarchive          { Option::OPT_FLAG, "--justarchive",           "-a",   "" };
 const Option opt_justmove             { Option::OPT_FLAG, "--justmove",              "-m",   "" };
@@ -598,8 +600,8 @@ const std::map<cmd_key_t, cmd_val_t> cmdOptions = {
         opt_state.optional(), opt_reason.optional(), opt_comment.optional() }},
    {{ AdminCmd::CMD_TAPE,                 AdminCmd::SUBCMD_CH    },
       { opt_vid, opt_mediatype.optional(), opt_vendor.optional(), opt_logicallibrary.optional(),
-        opt_tapepool.optional(), opt_encryptionkeyname.optional(),
-        opt_full.optional(), opt_state.optional(), opt_reason.optional(), opt_comment.optional() }},
+        opt_tapepool.optional(), opt_encryptionkeyname.optional(), opt_full.optional(), 
+        opt_state.optional(), opt_reason.optional(), opt_comment.optional(), opt_dirtybit.optional() }},
    {{ AdminCmd::CMD_TAPE,                 AdminCmd::SUBCMD_RM    }, { opt_vid }},
    {{ AdminCmd::CMD_TAPE,                 AdminCmd::SUBCMD_RECLAIM }, { opt_vid }},
    {{ AdminCmd::CMD_TAPE,                 AdminCmd::SUBCMD_LS    },
diff --git a/xroot_plugins/XrdCtaTapeLs.hpp b/xroot_plugins/XrdCtaTapeLs.hpp
index a6e48eeec3c4ea8b74f13e14bb59b74486230977..73ca91ce18f616b6587d861089d2f038d2f8105b 100644
--- a/xroot_plugins/XrdCtaTapeLs.hpp
+++ b/xroot_plugins/XrdCtaTapeLs.hpp
@@ -110,6 +110,7 @@ int TapeLsStream::fillBuffer(XrdSsiPb::OStreamBuffer<Data> *streambuf) {
     tape_item->set_occupancy(tape.dataOnTapeInBytes);
     tape_item->set_last_fseq(tape.lastFSeq);
     tape_item->set_full(tape.full);
+    tape_item->set_dirty(tape.dirty);
     tape_item->set_from_castor(tape.isFromCastor);
     tape_item->set_read_mount_count(tape.readMountCount);
     tape_item->set_write_mount_count(tape.writeMountCount);
diff --git a/xroot_plugins/XrdSsiCtaRequestMessage.cpp b/xroot_plugins/XrdSsiCtaRequestMessage.cpp
index cbf88168db2a93baf677b0224ba1341dba9d6987..ed9c91112cf1e7359c377c1da6a7a341ea8a859f 100644
--- a/xroot_plugins/XrdSsiCtaRequestMessage.cpp
+++ b/xroot_plugins/XrdSsiCtaRequestMessage.cpp
@@ -1891,6 +1891,7 @@ void RequestMessage::processTape_Ch(cta::xrd::Response &response)
    auto  full              = getOptional(OptionBoolean::FULL);
    auto  state             = getOptional(OptionString::STATE);
    auto  stateReason       = getOptional(OptionString::REASON);
+   auto  dirty             = getOptional(OptionBoolean::DIRTY_BIT);
 
    if(mediaType) {
       m_catalogue.modifyTapeMediaType(m_cliIdentity, vid, mediaType.value());
@@ -1921,6 +1922,9 @@ void RequestMessage::processTape_Ch(cta::xrd::Response &response)
      auto stateEnumValue = common::dataStructures::Tape::stringToState(state.value());
      m_catalogue.modifyTapeState(m_cliIdentity,vid,stateEnumValue,stateReason);
    }
+   if (dirty) {
+      m_catalogue.setTapeDirty(m_cliIdentity, vid, dirty.value());
+   }
 
    response.set_type(cta::xrd::Response::RSP_SUCCESS);
 }
diff --git a/xrootd-ssi-protobuf-interface b/xrootd-ssi-protobuf-interface
index 8ff426526ce7c9b05559761be6c006fe6bbd2cc4..6b3ffe5a8e358e1bff044aba877c4610867ff54f 160000
--- a/xrootd-ssi-protobuf-interface
+++ b/xrootd-ssi-protobuf-interface
@@ -1 +1 @@
-Subproject commit 8ff426526ce7c9b05559761be6c006fe6bbd2cc4
+Subproject commit 6b3ffe5a8e358e1bff044aba877c4610867ff54f