diff --git a/src/Io/lcio2/include/ACTFW/Io/lcio2/Lcio2TrackerHitWriter.hpp b/src/Io/lcio2/include/ACTFW/Io/lcio2/Lcio2TrackerHitWriter.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..d3263e072a66671e39f09269c433bc7dda1925f4
--- /dev/null
+++ b/src/Io/lcio2/include/ACTFW/Io/lcio2/Lcio2TrackerHitWriter.hpp
@@ -0,0 +1,96 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2018 CERN for the benefit of the Acts project
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#pragma once
+
+#include <cstdint>
+#include <mutex>
+#include <string>
+
+#include "ACTFW/EventData/SimHit.hpp"
+#include "ACTFW/Framework/WriterT.hpp"
+#include "lcio2/TrackerHitData.h"
+
+class TFile;
+class TTree;
+
+namespace FW {
+
+/// Write out simulated hits as a flat TTree.
+///
+/// Each entry in the TTree corresponds to one hit for optimum writing
+/// speed. The event number is part of the written data.
+///
+/// Safe to use from multiple writer threads. To avoid thread-saftey issues,
+/// the writer must be the sole owner of the underlying file. Thus, the
+/// output file pointer can not be given from the outside.
+class Lcio2TrackerHitWriter final : public WriterT<SimHitContainer> {
+ public:
+  struct Config {
+    /// Input particle collection to write.
+    std::string inputSimulatedHits;
+    /// Path to the output file.
+    std::string filePath;
+    /// Output file access mode.
+    std::string fileMode = "RECREATE";
+    /// Name of the tree within the output file.
+    std::string treeName = "hits";
+  };
+
+  /// Construct the particle writer.
+  ///
+  /// @params cfg is the configuration object
+  /// @params lvl is the logging level
+  Lcio2TrackerHitWriter(const Config& cfg, Acts::Logging::Level lvl);
+
+  /// Ensure underlying file is closed.
+  ~Lcio2TrackerHitWriter() final override;
+
+  /// End-of-run hook
+  ProcessCode endRun() final override;
+
+ protected:
+  /// Type-specific write implementation.
+  ///
+  /// @param[in] ctx is the algorithm context
+  /// @param[in] hits are the hits to be written
+  ProcessCode writeT(const AlgorithmContext& ctx,
+                     const SimHitContainer& hits) final override;
+
+ private:
+  Config m_cfg;
+  std::mutex m_writeMutex;
+  TFile* m_outputFile = nullptr;
+  TTree* m_outputTree = nullptr;
+  /// Event identifier.
+  uint32_t m_eventId;
+  /// Hit surface identifier.
+  uint64_t m_geometryId;
+  /// Event-unique particle identifier a.k.a. barcode.
+  uint64_t m_particleId;
+  /// True global hit position components in mm.
+  float m_tx, m_ty, m_tz;
+  // True global hit time in ns.
+  float m_tt;
+  /// True particle four-momentum in GeV at hit position before interaction.
+  float m_tpx, m_tpy, m_tpz, m_te;
+  /// True change in particle four-momentum in GeV due to interactions.
+  float m_deltapx, m_deltapy, m_deltapz, m_deltae;
+  /// Hit index along the particle trajectory
+  int32_t m_index;
+  // Decoded hit surface identifier components.
+  uint32_t m_volumeId;
+  uint32_t m_boundaryId;
+  uint32_t m_layerId;
+  uint32_t m_approachId;
+  uint32_t m_sensitiveId;
+  TrackerHitData
+
+};
+
+}  // namespace FW
diff --git a/src/Io/lcio2/src/Lcio2TrackerHitWriter.cpp b/src/Io/lcio2/src/Lcio2TrackerHitWriter.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..69b5a141b1fe1d4160d1ded307ea26f6c2152c71
--- /dev/null
+++ b/src/Io/lcio2/src/Lcio2TrackerHitWriter.cpp
@@ -0,0 +1,123 @@
+// This file is part of the Acts project.
+//
+// Copyright (C) 2017-2018 CERN for the benefit of the Acts project
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include "ACTFW/Io/Root/Lcio2TrackerHitWriter.hpp"
+
+#include <TFile.h>
+#include <TTree.h>
+#include <ios>
+#include <stdexcept>
+
+#include "Acts/Utilities/Units.hpp"
+
+FW::Lcio2TrackerHitWriter::Lcio2TrackerHitWriter(const FW::Lcio2TrackerHitWriter::Config& cfg,
+                                       Acts::Logging::Level lvl)
+    : WriterT(cfg.inputSimulatedHits, "Lcio2TrackerHitWriter", lvl), m_cfg(cfg) {
+  // inputParticles is already checked by base constructor
+  if (m_cfg.filePath.empty()) {
+    throw std::invalid_argument("Missing file path");
+  }
+  if (m_cfg.treeName.empty()) {
+    throw std::invalid_argument("Missing tree name");
+  }
+
+  // open root file and create the tree
+  m_outputFile = TFile::Open(m_cfg.filePath.c_str(), m_cfg.fileMode.c_str());
+  if (m_outputFile == nullptr) {
+    throw std::ios_base::failure("Could not open '" + m_cfg.filePath + "'");
+  }
+  m_outputFile->cd();
+  m_outputTree = new TTree(m_cfg.treeName.c_str(), m_cfg.treeName.c_str());
+  if (m_outputTree == nullptr) {
+    throw std::bad_alloc();
+  }
+
+  // setup the branches
+  m_outputTree->Branch("event_id", &m_eventId);
+  m_outputTree->Branch("geometry_id", &m_geometryId, "geometry_id/l");
+  m_outputTree->Branch("particle_id", &m_particleId, "particle_id/l");
+  m_outputTree->Branch("tx", &m_tx);
+  m_outputTree->Branch("ty", &m_ty);
+  m_outputTree->Branch("tz", &m_tz);
+  m_outputTree->Branch("tt", &m_tt);
+  m_outputTree->Branch("tpx", &m_tpx);
+  m_outputTree->Branch("tpy", &m_tpy);
+  m_outputTree->Branch("tpz", &m_tpz);
+  m_outputTree->Branch("te", &m_te);
+  m_outputTree->Branch("deltapx", &m_deltapx);
+  m_outputTree->Branch("deltapy", &m_deltapy);
+  m_outputTree->Branch("deltapz", &m_deltapz);
+  m_outputTree->Branch("deltae", &m_deltae);
+  m_outputTree->Branch("index", &m_index);
+  m_outputTree->Branch("volume_id", &m_volumeId);
+  m_outputTree->Branch("boundary_id", &m_boundaryId);
+  m_outputTree->Branch("layer_id", &m_layerId);
+  m_outputTree->Branch("approach_id", &m_approachId);
+  m_outputTree->Branch("sensitive_id", &m_sensitiveId);
+}
+
+FW::Lcio2TrackerHitWriter::~Lcio2TrackerHitWriter() {
+  if (m_outputFile) {
+    m_outputFile->Close();
+  }
+}
+
+FW::ProcessCode FW::Lcio2TrackerHitWriter::endRun() {
+  if (m_outputFile) {
+    m_outputFile->cd();
+    m_outputTree->Write();
+    ACTS_VERBOSE("Wrote hits to tree '" << m_cfg.treeName << "' in '"
+                                        << m_cfg.filePath << "'");
+  }
+  return ProcessCode::SUCCESS;
+}
+
+FW::ProcessCode FW::Lcio2TrackerHitWriter::writeT(const AlgorithmContext& ctx,
+                                             const FW::SimHitContainer& hits) {
+  if (not m_outputFile) {
+    ACTS_ERROR("Missing output file");
+    return ProcessCode::ABORT;
+  }
+
+  // ensure exclusive access to tree/file while writing
+  std::lock_guard<std::mutex> lock(m_writeMutex);
+
+  // Get the event number
+  m_eventId = ctx.eventNumber;
+  for (const auto& hit : hits) {
+    m_particleId = hit.particleId().value();
+    m_geometryId = hit.geometryId().value();
+    // write hit position
+    m_tx = hit.position4().x() / Acts::UnitConstants::mm;
+    m_ty = hit.position4().y() / Acts::UnitConstants::mm;
+    m_tz = hit.position4().z() / Acts::UnitConstants::mm;
+    m_tt = hit.position4().w() / Acts::UnitConstants::ns;
+    // write four-momentum before interaction
+    m_tpx = hit.momentum4Before().x() / Acts::UnitConstants::GeV;
+    m_tpy = hit.momentum4Before().y() / Acts::UnitConstants::GeV;
+    m_tpz = hit.momentum4Before().z() / Acts::UnitConstants::GeV;
+    m_te = hit.momentum4Before().w() / Acts::UnitConstants::GeV;
+    // write four-momentum change due to interaction
+    const auto delta4 = hit.momentum4After() - hit.momentum4Before();
+    m_deltapx = delta4.x() / Acts::UnitConstants::GeV;
+    m_deltapy = delta4.y() / Acts::UnitConstants::GeV;
+    m_deltapz = delta4.z() / Acts::UnitConstants::GeV;
+    m_deltae = delta4.w() / Acts::UnitConstants::GeV;
+    // write hit index along trajectory
+    m_index = hit.index();
+    // decoded geometry for simplicity
+    m_volumeId = hit.geometryId().volume();
+    m_boundaryId = hit.geometryId().boundary();
+    m_layerId = hit.geometryId().layer();
+    m_approachId = hit.geometryId().approach();
+    m_sensitiveId = hit.geometryId().sensitive();
+    // Fill the tree
+    m_outputTree->Fill();
+  }
+  return FW::ProcessCode::SUCCESS;
+}