diff --git a/corsika/detail/framework/process/InteractionHistogram.inl b/corsika/detail/framework/process/InteractionHistogram.inl
index ecb582368df13fc4efdb93347878148773b578f6..e6cfae869ea9ba6a9c8adc36d4e817a872822609 100644
--- a/corsika/detail/framework/process/InteractionHistogram.inl
+++ b/corsika/detail/framework/process/InteractionHistogram.inl
@@ -42,14 +42,6 @@ namespace corsika {
     }
   }
 
-  void InteractionHistogram::saveLab(std::string const& filename, SaveMode mode) const {
-    corsika::save_hist(inthist_lab_, filename, mode);
-  }
-
-  void InteractionHistogram::saveCMS(std::string const& filename, SaveMode mode) const {
-    corsika::save_hist(inthist_cms_, filename, mode);
-  }
-
   InteractionHistogram& InteractionHistogram::operator+=(
       InteractionHistogram const& other) {
     inthist_lab_ += other.inthist_lab_;
@@ -64,4 +56,4 @@ namespace corsika {
 
     return other;
   }
-} // namespace corsika
\ No newline at end of file
+} // namespace corsika
diff --git a/corsika/detail/framework/utility/SaveBoostHistogram.inl b/corsika/detail/framework/utility/SaveBoostHistogram.inl
index 94e5e1bbc8a383f99ef6a70776e622e6f0f2ad28..817fd6714a0a00ba492c31e9a45fa7c89ac2b5ff 100644
--- a/corsika/detail/framework/utility/SaveBoostHistogram.inl
+++ b/corsika/detail/framework/utility/SaveBoostHistogram.inl
@@ -11,6 +11,7 @@
 #include <cnpy.hpp>
 
 #include <boost/histogram.hpp>
+#include <boost/filesystem.hpp> // can be changed to std::filesystem if compiler supports it
 
 #include <functional>
 #include <memory>
@@ -23,11 +24,19 @@ namespace corsika {
 
   template <class Axes, class Storage>
   inline void save_hist(boost::histogram::histogram<Axes, Storage> const& h,
-                        std::string const& filename, SaveMode mode) {
-    unsigned const rank = h.rank();
+                        std::string const& filename, bool overwrite) {
+    if (boost::filesystem::status(filename).type() !=
+        boost::filesystem::file_type::file_not_found) {
+      if (overwrite) {
+        boost::filesystem::remove(filename);
+      } else {
+        using namespace std::literals;
+        throw std::runtime_error(
+            ("save_hist(): "s + filename + " already exists"s).c_str());
+      }
+    }
 
-    // append vs. overwrite
-    const std::string mode_str = (mode == SaveMode::append ? "a" : "w");
+    unsigned const rank = h.rank();
 
     std::vector<size_t> axes_dims;
     axes_dims.reserve(rank);
@@ -58,7 +67,7 @@ namespace corsika {
         ax_edges.push_back(ax.bin(ax.size() - 1).upper());
 
         cnpy::npz_save(filename, std::string{"binedges_"} + std::to_string(i),
-                       ax_edges.data(), {ax_edges.size()}, mode_str);
+                       ax_edges.data(), {ax_edges.size()}, "a");
       } else {
         axis_types.push_back('d');
         std::vector<int64_t> bins; // we assume that discrete axes have integer bins
@@ -67,14 +76,13 @@ namespace corsika {
         for (int j = 0; j < ax.size(); ++j) { bins.push_back(ax.bin(j).lower()); }
 
         cnpy::npz_save(filename, std::string{"bins_"} + std::to_string(i), bins.data(),
-                       {bins.size()}, mode_str);
+                       {bins.size()}, "a");
       }
     }
 
-    cnpy::npz_save(filename, std::string{"axistypes"}, axis_types.data(), {rank},
-                   mode_str);
-    cnpy::npz_save(filename, std::string{"overflow"}, overflow.get(), {rank}, mode_str);
-    cnpy::npz_save(filename, std::string{"underflow"}, underflow.get(), {rank}, mode_str);
+    cnpy::npz_save(filename, std::string{"axistypes"}, axis_types.data(), {rank}, "a");
+    cnpy::npz_save(filename, std::string{"overflow"}, overflow.get(), {rank}, "a");
+    cnpy::npz_save(filename, std::string{"underflow"}, underflow.get(), {rank}, "a");
 
     auto const prod_axis_size = std::accumulate(axes_dims.cbegin(), axes_dims.cend(),
                                                 unsigned{1}, std::multiplies<>());
@@ -98,9 +106,9 @@ namespace corsika {
       temp[p] = *x;
     }
 
-    cnpy::npz_save(filename, "data", temp.get(), axes_dims, mode_str);
+    cnpy::npz_save(filename, "data", temp.get(), axes_dims, "a");
     // In Python this array can directly be assigned to a histogram view if that
     // histogram has its axes correspondingly: hist.view(flow=True)[:] = file['data']
   } // namespace corsika
 
-} // namespace corsika
\ No newline at end of file
+} // namespace corsika
diff --git a/corsika/framework/process/InteractionHistogram.hpp b/corsika/framework/process/InteractionHistogram.hpp
index 667ba3b9820d29bfdbe32b7d7a5643a4699f1a95..ab0fee8fd299a36d3f150db1e8d0f1213a615315 100644
--- a/corsika/framework/process/InteractionHistogram.hpp
+++ b/corsika/framework/process/InteractionHistogram.hpp
@@ -43,9 +43,6 @@ namespace corsika {
     hist_type const& CMSHist() const { return inthist_cms_; }
     hist_type const& labHist() const { return inthist_lab_; }
 
-    void saveLab(std::string const& filename, SaveMode mode = SaveMode::append) const;
-    void saveCMS(std::string const& filename, SaveMode mode = SaveMode::append) const;
-
     InteractionHistogram& operator+=(InteractionHistogram const& other);
     InteractionHistogram operator+(InteractionHistogram other) const;
   };
diff --git a/corsika/framework/utility/SaveBoostHistogram.hpp b/corsika/framework/utility/SaveBoostHistogram.hpp
index 13a079410e7c4735e88eb0ec56f5346f96ad2407..90a5621bd54715964a117b041380d20470cf4af3 100644
--- a/corsika/framework/utility/SaveBoostHistogram.hpp
+++ b/corsika/framework/utility/SaveBoostHistogram.hpp
@@ -12,8 +12,6 @@
 
 namespace corsika {
 
-  enum class SaveMode { overwrite, append };
-
   /**
    * This functions saves a boost::histogram into a numpy file. Only rather basic axis
    * types are supported: regular, variable, integer, category<int>. Only "ordinary" bin
@@ -21,10 +19,13 @@ namespace corsika {
    *
    * Note that this function makes a temporary, dense copy of the histogram, which could
    * be an issue for huge sizes (e.g. for high dimensions)
+   *
+   * @param overwrite silently overwrite existing files if true, otherwise throw
+   * runtime_error
    */
   template <class Axes, class Storage>
   inline void save_hist(boost::histogram::histogram<Axes, Storage> const& h,
-                        std::string const& filename, SaveMode mode = SaveMode::append);
+                        std::string const& filename, bool overwrite = true);
 } // namespace corsika
 
 #include <corsika/detail/framework/utility/SaveBoostHistogram.inl>
diff --git a/examples/em_shower.cpp b/examples/em_shower.cpp
index 855fc5a388ed59e7e1b037cf077c6ca580feac5e..edb377953427cb89120828193319b41ce152d8b4 100644
--- a/examples/em_shower.cpp
+++ b/examples/em_shower.cpp
@@ -7,6 +7,7 @@
  */
 
 #include <corsika/framework/core/Cascade.hpp>
+#include <corsika/framework/utility/SaveBoostHistogram.hpp>
 #include <corsika/framework/geometry/Plane.hpp>
 #include <corsika/framework/geometry/Sphere.hpp>
 #include <corsika/framework/geometry/PhysicalGeometry.hpp>
@@ -180,7 +181,7 @@ int main(int argc, char** argv) {
   em_continuous.reset();
 
   auto const hists = proposalCounted.getHistogram();
-  hists.saveLab("inthist_lab_emShower.npz");
-  hists.saveCMS("inthist_cms_emShower.npz");
+  save_hist(hists.labHist(), "inthist_lab_emShower.npz", true);
+  save_hist(hists.CMSHist(), "inthist_cms_emShower.npz", true);
   longprof.save("longprof_emShower.txt");
 }
diff --git a/examples/hybrid_MC.cpp b/examples/hybrid_MC.cpp
index b4eab698c94feb5a182ea7c068272eb050b88289..71d8cf82875f39c85a7f6b11dd39f7ce2faac5bf 100644
--- a/examples/hybrid_MC.cpp
+++ b/examples/hybrid_MC.cpp
@@ -13,6 +13,7 @@
 #include <corsika/framework/process/InteractionCounter.hpp>
 /* clang-format on */
 #include <corsika/framework/geometry/Plane.hpp>
+#include <corsika/framework/utility/SaveBoostHistogram.hpp>
 #include <corsika/framework/geometry/Sphere.hpp>
 #include <corsika/framework/core/Logging.hpp>
 #include <corsika/framework/process/ProcessSequence.hpp>
@@ -267,11 +268,8 @@ int main(int argc, char** argv) {
   auto const hists = sibyllCounted.getHistogram() + sibyllNucCounted.getHistogram() +
                      urqmdCounted.getHistogram();
 
-  hists.saveLab("inthist_lab.txt");
-  hists.saveCMS("inthist_cms.txt");
-
-  hists.saveLab("inthist_lab.txt");
-  hists.saveCMS("inthist_cms.txt");
+  save_hist(hists.labHist(), "inthist_lab_hybrid.npz", true);
+  save_hist(hists.CMSHist(), "inthist_cms_hybrid.npz", true);
 
   longprof.save("longprof.txt");
 
diff --git a/examples/vertical_EAS.cpp b/examples/vertical_EAS.cpp
index 7d0a97d062940867454349e88f9520769f4ad0fd..28488a1bdb53a8cc9a6b8f234152acf289919dfa 100644
--- a/examples/vertical_EAS.cpp
+++ b/examples/vertical_EAS.cpp
@@ -15,6 +15,7 @@
 #include <corsika/framework/geometry/Plane.hpp>
 #include <corsika/framework/geometry/Sphere.hpp>
 #include <corsika/framework/core/Logging.hpp>
+#include <corsika/framework/utility/SaveBoostHistogram.hpp>
 #include <corsika/framework/process/ProcessSequence.hpp>
 #include <corsika/framework/process/SwitchProcessSequence.hpp>
 #include <corsika/framework/process/InteractionCounter.hpp>
@@ -281,7 +282,7 @@ int main(int argc, char** argv) {
   auto const hists = sibyllCounted.getHistogram() + sibyllNucCounted.getHistogram() +
                      urqmdCounted.getHistogram() + proposalCounted.getHistogram();
 
-  hists.saveLab("inthist_lab_verticalEAS.npz");
-  hists.saveCMS("inthist_cms_verticalEAS.npz");
+  save_hist(hists.labHist(), "inthist_lab_verticalEAS.npz", true);
+  save_hist(hists.CMSHist(), "inthist_cms_verticalEAS.npz", true);
   longprof.save("longprof_verticalEAS.txt");
 }
diff --git a/tests/framework/testInteractionCounter.cpp b/tests/framework/testInteractionCounter.cpp
index b9cb8b013bbc94dd883367c6bafd4c41b91ae7f1..bf73fc578316eb3c7e948c22671f6a1aad220ddc 100644
--- a/tests/framework/testInteractionCounter.cpp
+++ b/tests/framework/testInteractionCounter.cpp
@@ -21,6 +21,11 @@
 #include <catch2/catch.hpp>
 
 #include <numeric>
+#include <algorithm>
+#include <iterator>
+#include <string>
+#include <fstream>
+#include <cstdio>
 
 using namespace corsika;
 
@@ -69,10 +74,31 @@ TEST_CASE("InteractionCounter", "[process]") {
     CHECK(h2.at(h2.axis(0).index(1'000'070'140), h2.axis(1).index(1.6e12)) == 1);
     CHECK(std::accumulate(h2.cbegin(), h2.cend(), 0) == 1);
 
-    countedProcess.getHistogram().saveLab("testInteractionCounter_file1.npz",
-                                          SaveMode::overwrite);
-    countedProcess.getHistogram().saveCMS("testInteractionCounter_file2.npz",
-                                          SaveMode::overwrite);
+    save_hist(countedProcess.getHistogram().labHist(), "testInteractionCounter_file1.npz",
+              true);
+    save_hist(countedProcess.getHistogram().CMSHist(), "testInteractionCounter_file2.npz",
+              true);
+
+    SECTION("output validation") {
+      auto const file = GENERATE(as<std::string>{}, "testInteractionCounter_file1",
+                                 "testInteractionCounter_file2");
+
+      std::cout << file + ".npz vs " << refDataDir + "/" + file + "_REF.npz" << std::endl;
+
+      // compare to binary reference data
+      // note that this currenly compares the whole files byte by byte. If the new
+      // or the reference file are compressed this would be a false-negative outcome
+      // of this test
+      std::ifstream file1(file + ".npz");
+      std::ifstream file1ref(refDataDir + "/" + file + "_REF.npz");
+
+      std::istreambuf_iterator<char> begin1(file1);
+      std::istreambuf_iterator<char> begin1ref(file1ref);
+
+      std::istreambuf_iterator<char> end;
+
+      CHECK(std::equal(begin1, end, begin1ref));
+    }
   }
 
   SECTION("DoInteraction Lambda") {
@@ -93,41 +119,3 @@ TEST_CASE("InteractionCounter", "[process]") {
     CHECK(std::accumulate(h2.cbegin(), h2.cend(), 0) == 1);
   }
 }
-
-#include <algorithm>
-#include <iterator>
-#include <string>
-#include <fstream>
-
-TEST_CASE("InteractionCounterOutput", "[output validation]") {
-
-  logging::set_level(logging::level::info);
-  corsika_logger->set_pattern("[%n:%^%-8l%$] custom pattern: %v");
-
-  auto file = GENERATE(as<std::string>{}, "testInteractionCounter_file1",
-                       "testInteractionCounter_file2");
-
-  SECTION(std::string("check saved data, ") + file + ".npz") {
-
-    std::cout << file + ".npz vs " << refDataDir + "/" + file + "_REF.npz" << std::endl;
-
-    // compare to binary reference data
-    std::ifstream file1(file + ".npz");
-    std::ifstream file1ref(refDataDir + "/" + file + "_REF.npz");
-
-    std::istreambuf_iterator<char> begin1(file1);
-    std::istreambuf_iterator<char> begin1ref(file1ref);
-
-    std::istreambuf_iterator<char> end;
-
-    while (begin1 != end && begin1ref != end) {
-      CHECK(*begin1 == *begin1ref);
-      ++begin1;
-      ++begin1ref;
-    }
-    CHECK(begin1 == end);
-    CHECK(begin1ref == end);
-    file1.close();
-    file1ref.close();
-  }
-}
diff --git a/tests/framework/testInteractionCounter_file1_REF.npz b/tests/framework/testInteractionCounter_file1_REF.npz
index 13e2d5d49e16b6a2e7eb6b95dc1a26bccab18ae8..ca75c99e4df0292da9db9f05f5a7b3319c482653 100644
Binary files a/tests/framework/testInteractionCounter_file1_REF.npz and b/tests/framework/testInteractionCounter_file1_REF.npz differ
diff --git a/tests/framework/testInteractionCounter_file2_REF.npz b/tests/framework/testInteractionCounter_file2_REF.npz
index 8509b27e4d690a386c2710a24ceda6a1143bf757..2aaa26ed6240a2de96ae24cc566d695d7f4467b1 100644
Binary files a/tests/framework/testInteractionCounter_file2_REF.npz and b/tests/framework/testInteractionCounter_file2_REF.npz differ
diff --git a/tests/framework/testSaveBoostHistogram.cpp b/tests/framework/testSaveBoostHistogram.cpp
index 91bac8b82120b0ce2595a0f8d91a5f503706403c..d5c16438a2ab6de1595393205d4eff4320a43d08 100644
--- a/tests/framework/testSaveBoostHistogram.cpp
+++ b/tests/framework/testSaveBoostHistogram.cpp
@@ -40,5 +40,6 @@ TEST_CASE("SaveHistogram") {
     h(a, b, c, d);
   }
 
-  corsika::save_hist(h, "hist.npz");
+  REQUIRE_NOTHROW(corsika::save_hist(h, "hist.npz", true));
+  REQUIRE_THROWS(corsika::save_hist(h, "hist.npz", false));
 }
diff --git a/tests/modules/testInteractionCounter.cpp b/tests/modules/testInteractionCounter.cpp
deleted file mode 100644
index 350e1dc1de38a96a69d91941a37f0fb371eaccc3..0000000000000000000000000000000000000000
--- a/tests/modules/testInteractionCounter.cpp
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * (c) Copyright 2021 CORSIKA Project, corsika-project@lists.kit.edu
- *
- * This software is distributed under the terms of the GNU General Public
- * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
- * the license.
- */
-
-#include <corsika/modules/InteractionCounter.hpp>
-
-#include <corsika/media/Environment.hpp>
-#include <corsika/media/HomogeneousMedium.hpp>
-#include <corsika/media/NuclearComposition.hpp>
-#include <corsika/framework/geometry/Point.hpp>
-#include <corsika/framework/geometry/RootCoordinateSystem.hpp>
-#include <corsika/framework/geometry/Vector.hpp>
-#include <corsika/framework/core/PhysicalUnits.hpp>
-
-#include <corsika/setup/SetupStack.hpp>
-
-#include <catch2/catch.hpp>
-
-#include <numeric>
-
-using namespace corsika;
-
-const std::string refDataDir = std::string(REFDATADIR); // from cmake
-
-struct DummyProcess {
-  template <typename TParticle>
-  GrammageType GetInteractionLength([[maybe_unused]] TParticle const& particle) {
-    return 100_g / 1_cm / 1_cm;
-  }
-
-  template <typename TParticle>
-  auto DoInteraction([[maybe_unused]] TParticle& projectile) {
-    return nullptr;
-  }
-};
-
-TEST_CASE("InteractionCounter", "[process]") {
-
-  logging::set_level(logging::level::info);
-  corsika_logger->set_pattern("[%n:%^%-8l%$] custom pattern: %v");
-
-  DummyProcess d;
-  InteractionCounter countedProcess(d);
-
-  SECTION("GetInteractionLength") {
-    CHECK(countedProcess.GetInteractionLength(nullptr) == 100_g / 1_cm / 1_cm);
-  }
-
-  auto [env, csPtr, nodePtr] = setup::testing::setupEnvironment(particles::Code::Oxygen);
-  [[maybe_unused]] auto& env_dummy = env;
-
-  SECTION("DoInteraction nucleus") {
-    unsigned short constexpr A = 14, Z = 7;
-    auto [stackPtr, secViewPtr] = setup::testing::setupStack(particles::Code::Nucleus, A,
-                                                             Z, 105_TeV, nodePtr, *csPtr);
-    CHECK(stackPtr->getEntries() == 1);
-    CHECK(secViewPtr->getEntries() == 0);
-
-    auto const ret = countedProcess.DoInteraction(*secViewPtr);
-    CHECK(ret == nullptr);
-
-    auto const& h = countedProcess.GetHistogram().labHist();
-    CHECK(h.at(h.axis(0).index(1'000'070'140), h.axis(1).index(1.05e14)) == 1);
-    CHECK(std::accumulate(h.cbegin(), h.cend(), 0) == 1);
-
-    auto const& h2 = countedProcess.GetHistogram().CMSHist();
-    CHECK(h2.at(h2.axis(0).index(1'000'070'140), h2.axis(1).index(1.6e12)) == 1);
-    CHECK(std::accumulate(h2.cbegin(), h2.cend(), 0) == 1);
-
-    countedProcess.GetHistogram().saveLab("testInteractionCounter_file1.npz",
-                                          utl::SaveMode::overwrite);
-    countedProcess.GetHistogram().saveCMS("testInteractionCounter_file2.npz",
-                                          utl::SaveMode::overwrite);
-  }
-
-  SECTION("DoInteraction Lambda") {
-    auto constexpr code = particles::Code::Lambda0;
-    auto [stackPtr, secViewPtr] =
-        setup::testing::setupStack(code, 0, 0, 105_TeV, nodePtr, *csPtr);
-    CHECK(stackPtr->getEntries() == 1);
-    CHECK(secViewPtr->getEntries() == 0);
-
-    auto const ret = countedProcess.DoInteraction(*secViewPtr);
-    CHECK(ret == nullptr);
-
-    auto const& h = countedProcess.GetHistogram().labHist();
-    CHECK(h.at(h.axis(0).index(3122), h.axis(1).index(1.05e14)) == 1);
-    CHECK(std::accumulate(h.cbegin(), h.cend(), 0) == 1);
-
-    auto const& h2 = countedProcess.GetHistogram().CMSHist();
-    CHECK(h2.at(h2.axis(0).index(3122), h2.axis(1).index(1.6e12)) == 1);
-    CHECK(std::accumulate(h2.cbegin(), h2.cend(), 0) == 1);
-  }
-}
-
-#include <algorithm>
-#include <iterator>
-#include <string>
-#include <fstream>
-
-TEST_CASE("InteractionCounterOutput", "[output validation]") {
-
-  logging::set_level(logging::level::info);
-  corsika_logger->set_pattern("[%n:%^%-8l%$] custom pattern: %v");
-
-  auto file = GENERATE(as<std::string>{}, "testInteractionCounter_file1",
-                       "testInteractionCounter_file2");
-
-  SECTION(std::string("check saved data, ") + file + ".npz") {
-
-    std::cout << file + ".npz vs " << refDataDir + "/" + file + "_REF.npz" << std::endl;
-
-    // compare to binary reference data
-    std::ifstream file1(file + ".npz");
-    std::ifstream file1ref(refDataDir + "/" + file + "_REF.npz");
-
-    std::istreambuf_iterator<char> begin1(file1);
-    std::istreambuf_iterator<char> begin1ref(file1ref);
-
-    std::istreambuf_iterator<char> end;
-
-    while (begin1 != end && begin1ref != end) {
-      CHECK(*begin1 == *begin1ref);
-      ++begin1;
-      ++begin1ref;
-    }
-    CHECK(begin1 == end);
-    CHECK(begin1ref == end);
-    file1.close();
-    file1ref.close();
-  }
-}
diff --git a/tools/read_hist.py b/tools/read_hist.py
new file mode 100755
index 0000000000000000000000000000000000000000..184ba5e2e72e3d01a8e1e109731793f8cfc9c665
--- /dev/null
+++ b/tools/read_hist.py
@@ -0,0 +1,27 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import boost_histogram as bh
+
+def read_hist(filename):
+    """
+    read numpy file produced with CORSIKA 8's save_hist() function into
+    boost-histogram object.
+    """
+
+    d = np.load(filename)
+    axistypes = d['axistypes'].view('c')
+    overflow = d['overflow']
+    underflow = d['underflow']
+
+    axes = []
+    for i, (at, has_overflow, has_underflow) in enumerate(zip(axistypes, overflow, underflow)):
+        if at == b'c':
+            axes.append(bh.axis.Variable(d[f'binedges_{i}'], overflow=has_overflow, underflow=has_underflow))
+        elif at == b'd':
+            axes.append(bh.axis.IntCategory(d[f'bins_{i}'], growth=(not has_overflow)))
+ 
+    h = bh.Histogram(*axes)
+    h.view(flow=True)[:] = d['data']
+    
+    return h
+