diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000000000000000000000000000000000000..a14b2319185216425e40d41da6983c616c70f12c
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,18 @@
+HeaderFilterRegex: ".*"
+Checks: "-*,boost-*,bugprone-argument-comment,bugprone-assert-side-effect,bugprone-bad-signal-to-kill-thread,bugprone-bool-pointer-implicit-conversion,bugprone-branch-clone,bugprone-copy-constructor-init,bugprone-dangling-handle,bugprone-dynamic-static-initializers,bugprone-exception-escape,bugprone-fold-init-type,bugprone-forward-declaration-namespace,bugprone-forwarding-reference-overload,bugprone-implicit-widening-of-multiplication-result,bugprone-inaccurate-erase,bugprone-incorrect-roundings,bugprone-infinite-loop,bugprone-integer-division,bugprone-lambda-function-name,bugprone-macro-*,bugprone-misplaced-*,bugprone-move-forwarding-reference,bugprone-multiple-statement-macro,bugprone-narrowing-conversions,bugprone-no-escape,bugprone-not-null-terminated-result,bugprone-parent-virtual-call,bugprone-posix-return,bugprone-redundant-branch-condition,bugprone-reserved-identifier,bugprone-signal-handler,bugprone-signed-char-misuse,bugprone-sizeof-*,bugprone-spuriously-wake-up-functions,bugprone-string-*,bugprone-suspicious-*,bugprone-swapped-arguments,bugprone-terminating-continue,bugprone-throw-keyword-missing,bugprone-too-small-loop-variable,bugprone-undefined-memory-manipulation,bugprone-undelegated-constructor,bugprone-unhandled-*,bugprone-unused-*,bugprone-use-after-move,bugprone-virtual-near-miss,clang-*,cppcoreguidelines-interfaces-global-init,cppcoreguidelines-narrowing-conversions,cppcoreguidelines-no-malloc,cppcoreguidelines-owning-memory,cppcoreguidelines-pro-type-const-cast,cppcoreguidelines-pro-type-cstyle-cast,cppcoreguidelines-pro-type-member-init,cppcoreguidelines-pro-type-reinterpret-cast,cppcoreguidelines-pro-type-static-cast-downcast,cppcoreguidelines-pro-type-union-access,cppcoreguidelines-slicing,darwin-*,google-build-explicit-make-pair,google-build-namespaces,google-default-arguments,google-explicit-constructor,google-global-names-in-headers,google-objc-*,google-readability-avoid-underscore-in-googletest-name,readability-braces-around-statements,google-runtime-*,google-upgrade-googletest-case,hicpp-exception-baseclass,hicpp-multiway-paths-covered,hicpp-no-assembler,hicpp-signed-bitwise,linuxkernel-*,llvm-include-order,llvm-namespace-comment,llvm-prefer-*,llvm-twine-local,misc-definitions-in-headers,misc-misplaced-const,misc-new-delete-overloads,misc-non-copyable-objects,misc-redundant-expression,misc-static-assert,misc-throw-by-value-catch-by-reference,misc-unconventional-assign-operator,misc-uniqueptr-reset-release,misc-unused-*,modernize-avoid-bind,modernize-avoid-c-arrays,modernize-concat-nested-namespaces,modernize-deprecated-*,modernize-loop-convert,modernize-make-*,modernize-pass-by-value,modernize-raw-string-literal,modernize-redundant-void-arg,modernize-replace-*,modernize-return-braced-init-list,modernize-shrink-to-fit,modernize-unary-static-assert,modernize-use-auto,modernize-use-bool-literals,modernize-use-default-member-init,modernize-use-emplace,modernize-use-equals-*,modernize-use-nodiscard,modernize-use-noexcept,modernize-use-nullptr,modernize-use-override,modernize-use-transparent-functors,modernize-use-uncaught-exceptions,modernize-use-using,mpi-*,objc-*,openmp-*,performance-faster-string-find,performance-for-range-copy,performance-implicit-conversion-in-loop,performance-inefficient-algorithm,performance-inefficient-vector-operation,performance-move-*,performance-no-automatic-move,performance-noexcept-move-constructor,performance-trivially-destructible,performance-type-promotion-in-math-fn,performance-unnecessary-*,portability-*,readability-avoid-const-params-in-decls,readability-const-return-type,readability-container-size-empty,readability-convert-member-functions-to-static,readability-delete-null-pointer,readability-else-after-return,readability-function-size,readability-identifier-naming,readability-inconsistent-declaration-parameter-name,readability-make-member-function-const,readability-misleading-indentation,readability-misplaced-array-index,readability-non-const-parameter,readability-qualified-auto,readability-redundant-*,readability-simplify-*,readability-static-*,readability-string-compare,readability-uniqueptr-delete-release,readability-uppercase-literal-suffix,readability-use-anyofallof,zircon-*,missing-noreturn"
+
+CheckOptions:
+  - key: readability-identifier-naming.PrivateMemberPrefix
+    value: "_"
+  - key: readability-identifier-naming.ProtectedMemberPrefix
+    value: "_"
+  - key: readability-identifier-naming.MemberCase
+    value: "camelBack"
+  - key: readability-identifier-naming.FunctionCase
+    value: "camelBack"
+  - key: readability-identifier-naming.ClassCase
+    value: "CamelCase"
+  - key: readability-identifier-naming.PublicMemberPrefix
+    value: ""
+  - key: readability-identifier-naming.MacroDefinitionCase
+    value: "UPPER_CASE"
diff --git a/cmake/add_linter_target.cmake b/cmake/add_linter_target.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..80a504c2a57cc0ddd6849e05d3b1a39b362fbbd8
--- /dev/null
+++ b/cmake/add_linter_target.cmake
@@ -0,0 +1,13 @@
+set(CTK_CLANG_TIDY_VERSION "14" CACHE STRING "Version of the clang-tidy binary to use")
+set(CTK_ENABLE_TIDY_WHILE_BUILDING OFF CACHE BOOL "Whether to run clang-tidy on every compilation unit")
+if (CTK_ENABLE_TIDY_WHILE_BUILDING)
+    set(CMAKE_CXX_CLANG_TIDY /usr/bin/clang-tidy-${CTK_CLANG_TIDY_VERSION};-config-file=${CMAKE_SOURCE_DIR}/.clang-tidy)
+    set(CMAKE_C_CLANG_TIDY /usr/bin/clang-tidy-${CTK_CLANG_TIDY_VERSION};-config-file=${CMAKE_SOURCE_DIR}/.clang-tidy)
+endif()
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+add_custom_target(run-linter COMMAND run-clang-tidy-${CTK_CLANG_TIDY_VERSION} -p ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+add_custom_target(fix-linter-stepwise COMMAND ${CMAKE_SOURCE_DIR}/cmake/fix-linter-for-all.py --tidy=clang-tidy-${CTK_CLANG_TIDY_VERSION} --apply-tool=clang-apply-replacements-${CTK_CLANG_TIDY_VERSION} --exclude="${CMAKE_BINARY_DIR}" ${CMAKE_BINARY_DIR}/compile_commands.json
+    WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+add_custom_target(fix-linter COMMAND run-clang-tidy-${CTK_CLANG_TIDY_VERSION} -fix -format -p ${CMAKE_BINARY_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR})
+
+
diff --git a/cmake/fix-linter-for-all.py b/cmake/fix-linter-for-all.py
new file mode 100755
index 0000000000000000000000000000000000000000..e3746be9ec058bcf36f40aa7af8c799722f37224
--- /dev/null
+++ b/cmake/fix-linter-for-all.py
@@ -0,0 +1,90 @@
+#!/usr/bin/env python3
+# SPDX-License-Identifier: LGPL-3.0-or-later
+# SPDX-FileCopyRightText: Deutsches Elektronen-Synchrotron DESY, MSK, ChimeraTK Project <chimeratk-support@desy.de>
+
+import argparse
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+
+
+def make_absolute(file: str, path: str):
+    if os.path.isabs(file):
+        return file
+    return os.path.normpath(os.path.join(path, file))
+
+
+def main():
+    parser = argparse.ArgumentParser(description="Runs clang-tidy over a compilation database and applies the fixes")
+    parser.add_argument("compile_db", metavar='PATH')
+    parser.add_argument("--tidy", help="Path to clang-tidy", default="clang-tidy-14")
+    parser.add_argument("--apply-tool", help='Path to clang-apply-replacements', default="clang-apply-replacements-14")
+    parser.add_argument("--git", help="Path to git", default="git")
+    parser.add_argument("--source", help="Path to top-level source folder", default=".")
+    parser.add_argument("--exclude", help="Regex of files to exclude", default=None)
+
+    args = parser.parse_args()
+
+    apply_version = subprocess.run([args.apply_tool, "--version"], capture_output=True,
+                                   encoding="latin1").stdout.strip()
+
+    version_regex = re.compile(r"ersion\s*(\d+)\.(\d+)\.(\d+)")
+    match = version_regex.search(apply_version)
+    has_ignore_insert_conflict = int(match.group(1)) > 14
+
+    files = None
+
+    try:
+      database = json.load(open(args.compile_db))
+      files = set([make_absolute(entry['file'], entry['directory']) for entry in database])
+    except FileNotFoundError:
+        print(f"Failed to open {args.compile_db}: Not found")
+    except json.decoder.JSONDecodeError:
+        print(f"Failed to open {args.compile_db}: Not valid json")
+
+
+    if not files:
+        sys.exit(1)
+
+    tmpdir = tempfile.mkdtemp()
+    source_folder = os.path.abspath(args.source)
+
+    exclude_re = re.compile(args.exclude) if args.exclude else None
+
+    for file in files:
+        if exclude_re and exclude_re.search(file):
+            continue
+        tidy = [args.tidy, "-header-filter=.*", "-export-fixes"]
+        (handle, name) = tempfile.mkstemp(suffix='.yaml', dir=tmpdir)
+        os.close(handle)
+        tidy.append(name)
+        tidy.append("-p=" + os.path.dirname(os.path.normpath(args.compile_db)))
+        tidy.append(file)
+        try:
+            print(f"Linting {file}...")
+            subprocess.run(tidy, check=True)
+        except subprocess.CalledProcessError:
+            print("Linting failed. Usually that means the previously applied fix has introduced a compiler error. Aborting.")
+            print("Most likely it converted a conversion operator to explicit")
+            sys.exit(1)
+
+        fixer = [args.apply_tool, "--format", "--style=file", "--remove-change-desc-files"]
+        if has_ignore_insert_conflict:
+            fixer.append('--ignore-insert-conflict')
+        fixer.append(tmpdir)
+        if subprocess.call(fixer) == 0:
+            git_update = [args.git, 'add', '-u']
+            subprocess.call(git_update)
+            git_commit = [args.git, 'commit', '-m', f'clang-tidy: {os.path.relpath(file, start=source_folder)}']
+            subprocess.call(git_commit)
+        else:
+            print("Error applying fixes, not committing")
+    shutil.rmtree(tmpdir)
+
+
+if __name__ == "__main__":
+    main()