diff --git a/src/pystencilssfg/__init__.py b/src/pystencilssfg/__init__.py
index a888fc45ce84f318dfd51e92d74c04fda18d265a..3bea41febe6e23d9c91537a978212e26c071bc35 100644
--- a/src/pystencilssfg/__init__.py
+++ b/src/pystencilssfg/__init__.py
@@ -1,4 +1,3 @@
-from .configuration import SfgConfiguration, SfgOutputMode, SfgCodeStyle
 from .config import SfgConfig
 from .generator import SourceFileGenerator
 from .composer import SfgComposer
@@ -10,9 +9,6 @@ __all__ = [
     "SfgConfig",
     "SourceFileGenerator",
     "SfgComposer",
-    "SfgConfiguration",
-    "SfgOutputMode",
-    "SfgCodeStyle",
     "SfgContext",
     "SfgVar",
     "AugExpr",
diff --git a/src/pystencilssfg/cli.py b/src/pystencilssfg/cli.py
index e5bdb4d815f97cb5dcfb58208341ff91c26609ce..3b321c2d0be13b906384701080b8de870647d507 100644
--- a/src/pystencilssfg/cli.py
+++ b/src/pystencilssfg/cli.py
@@ -4,13 +4,8 @@ from os import path
 
 from argparse import ArgumentParser, BooleanOptionalAction
 
-from .configuration import (
-    SfgConfigException,
-    SfgConfigSource,
-    add_config_args_to_parser,
-    config_from_parser_args,
-    merge_configurations,
-)
+from .config import CommandLineParameters, SfgConfigException, OutputMode
+from .emission import OutputSpec
 
 
 def add_newline_arg(parser):
@@ -39,7 +34,7 @@ def cli_main(program="sfg-cli"):
     )
 
     outfiles_parser.set_defaults(func=list_files)
-    add_config_args_to_parser(outfiles_parser)
+    CommandLineParameters.add_args_to_parser(outfiles_parser)
     add_newline_arg(outfiles_parser)
     outfiles_parser.add_argument(
         "--sep", type=str, default=" ", dest="sep", help="Separator for list items"
@@ -79,21 +74,18 @@ def version(args):
 
 
 def list_files(args):
-    try:
-        project_config, cmdline_config = config_from_parser_args(args)
-    except SfgConfigException as exc:
-        abort_with_config_exception(exc)
-
-    config = merge_configurations(project_config, cmdline_config, None)
+    cli_params = CommandLineParameters(args)
+    config = cli_params.get_config()
 
     _, scriptname = path.split(args.codegen_script)
     basename = path.splitext(scriptname)[0]
 
-    from .emission import HeaderImplPairEmitter
-
-    emitter = HeaderImplPairEmitter(config.get_output_spec(basename))
+    output_spec = OutputSpec.create(config, basename)
+    output_files = [output_spec.get_header_filepath()]
+    if config.output_mode != OutputMode.HEADER_ONLY:
+        output_files.append(output_spec.get_impl_filepath())
 
-    print(args.sep.join(emitter.output_files), end=os.linesep if args.newline else "")
+    print(args.sep.join(output_files), end=os.linesep if args.newline else "")
 
     exit(0)
 
@@ -112,18 +104,6 @@ def make_cmake_find_module(args):
     exit(0)
 
 
-def abort_with_config_exception(exception: SfgConfigException):
-    def eprint(*args, **kwargs):
-        print(*args, file=sys.stderr, **kwargs)
-
-    match exception.config_source:
-        case SfgConfigSource.PROJECT:
-            eprint(
-                f"Invalid project configuration: {exception.message}\nCheck your configurator script."
-            )
-        case SfgConfigSource.COMMANDLINE:
-            eprint(f"Invalid configuration on command line: {exception.message}")
-        case _:
-            assert False, "(Theoretically) unreachable code. Contact the developers."
-
+def abort_with_config_exception(exception: SfgConfigException, source: str):
+    print(f"Invalid {source} configuration: {exception.args[0]}.", file=sys.stderr)
     exit(1)
diff --git a/src/pystencilssfg/config.py b/src/pystencilssfg/config.py
index 992233999a033b3596f722f950caba26206dacc4..9cbb5c5cd1c634ea156b253aea1b5d913dd60b5f 100644
--- a/src/pystencilssfg/config.py
+++ b/src/pystencilssfg/config.py
@@ -129,6 +129,19 @@ class CodeStyle(ConfigBase):
     indent_width: Option[int] = Option(2)
     """The number of spaces successively nested blocks should be indented with"""
 
+    #   TODO possible future options:
+    #    - newline before opening {
+    #    - trailing return types
+
+    def indent(self, s: str):
+        from textwrap import indent
+
+        prefix = " " * self.get_option("indent_width")
+        return indent(s, prefix)
+
+
+@dataclass
+class ClangFormatOptions(ConfigBase):
     code_style: Option[str] = Option("file")
     """Code style to be used by clang-format. Passed verbatim to `--style` argument of the clang-format CLI.
 
@@ -136,15 +149,31 @@ class CodeStyle(ConfigBase):
     tree will automatically be used.
     """
 
-    force_clang_format: Option[bool] = Option(False)
+    force: Option[bool] = Option(False)
     """If set to True, abort code generation if ``clang-format`` binary cannot be found."""
 
-    skip_clang_format: Option[bool] = Option(False)
+    skip: Option[bool] = Option(False)
     """If set to True, skip formatting using ``clang-format``."""
 
-    clang_format_binary: Option[str] = Option("clang-format")
+    binary: Option[str] = Option("clang-format")
     """Path to the clang-format executable"""
 
+    @force.validate
+    def _validate_force(self, val: bool) -> bool:
+        if val and self.skip:
+            raise SfgConfigException(
+                "Cannot set both `clang_format.force` and `clang_format.skip` at the same time"
+            )
+        return val
+
+    @skip.validate
+    def _validate_skip(self, val: bool) -> bool:
+        if val and self.force:
+            raise SfgConfigException(
+                "Cannot set both `clang_format.force` and `clang_format.skip` at the same time"
+            )
+        return val
+
 
 class _GlobalNamespace: ...  # noqa: E701
 
@@ -165,6 +194,9 @@ class SfgConfig(ConfigBase):
     (like ``a::b::c``) or `GLOBAL_NAMESPACE` if no outer namespace should be generated."""
 
     codestyle: CodeStyle = field(default_factory=CodeStyle)
+    """Options affecting the code style emitted by pystencils-sfg."""
+
+    clang_format: ClangFormatOptions = field(default_factory=ClangFormatOptions)
     """Options governing the code style used by the code generator"""
 
     output_directory: Option[str] = Option(".")
@@ -199,7 +231,7 @@ class CommandLineParameters:
 
         return parser
 
-    def __init__(self, args):
+    def __init__(self, args) -> None:
         self._cl_config_module_path: str | None = args.config_module_path
 
         if args.output_mode is not None:
@@ -228,6 +260,7 @@ class CommandLineParameters:
             self._cl_header_ext = None
             self._cl_impl_ext = None
 
+        self._config_module: ModuleType | None
         if self._cl_config_module_path is not None:
             self._config_module = self._import_config_module(
                 self._cl_config_module_path
@@ -319,48 +352,5 @@ class CommandLineParameters:
             )
 
         config_module = iutil.module_from_spec(cfg_spec)
-        cfg_spec.loader.exec_module(config_module)
+        cfg_spec.loader.exec_module(config_module)  # type: ignore
         return config_module
-
-
-def run_configuration_module(configurator_script: str) -> SfgConfig:
-    """Run a configuration module.
-
-    A configuration module must define a function called `configure_sfg`
-    with the following signature in its global namespace:
-
-    ```Python
-    def configure_sfg(cfg: SfgConfig) -> None: ...
-    ```
-
-    After importing the module, that function will be called.
-    It should populate the ``cfg`` object with the desired configuration
-    options.
-
-    TODO: The configuration module may optionally define a function ``project_info``,
-    which takes zero arguments and should return an containing project-specific information.
-    This object will later be available through the `project_info` member of the
-    `SfgContext`.
-    """
-
-    cfg_modulename = path.splitext(path.split(configurator_script)[1])[0]
-
-    cfg_spec = iutil.spec_from_file_location(cfg_modulename, configurator_script)
-
-    if cfg_spec is None:
-        raise SfgConfigException(
-            f"Unable to load configurator script {configurator_script}",
-        )
-
-    configurator = iutil.module_from_spec(cfg_spec)
-    cfg_spec.loader.exec_module(configurator)
-
-    if not hasattr(configurator, "configure_sfg"):
-        raise SfgConfigException(
-            "Project configurator does not define function `configure_sfg`.",
-        )
-
-    cfg = SfgConfig()
-    project_config = configurator.configure_sfg(cfg)
-
-    return project_config
diff --git a/src/pystencilssfg/configuration.py b/src/pystencilssfg/configuration.py
deleted file mode 100644
index a76251a04cde0d875b0ba41908a0fadc5fc90553..0000000000000000000000000000000000000000
--- a/src/pystencilssfg/configuration.py
+++ /dev/null
@@ -1,384 +0,0 @@
-# mypy: strict_optional=False
-
-from __future__ import annotations
-
-from typing import Sequence, Any
-from os import path
-from enum import Enum, auto
-from dataclasses import dataclass, replace, fields, InitVar
-from argparse import ArgumentParser
-from textwrap import indent
-
-from importlib import util as iutil
-
-from .exceptions import SfgException
-
-
-class SfgConfigSource(Enum):
-    DEFAULT = auto()
-    PROJECT = auto()
-    COMMANDLINE = auto()
-    SCRIPT = auto()
-
-
-class SfgConfigException(Exception):
-    def __init__(self, cfg_src: SfgConfigSource | None, message: str):
-        super().__init__(cfg_src, message)
-        self.message = message
-        self.config_source = cfg_src
-
-
-@dataclass
-class SfgCodeStyle:
-    indent_width: int = 2
-
-    code_style: str = "file"
-    """Code style to be used by clang-format. Passed verbatim to `--style` argument of the clang-format CLI.
-
-    Similar to clang-format itself, the default value is `file`, such that a `.clang-format` file found in the build
-    tree will automatically be used.
-    """
-
-    force_clang_format: bool = False
-    """If set to True, abort code generation if ``clang-format`` binary cannot be found."""
-
-    skip_clang_format: bool = False
-    """If set to True, skip formatting using ``clang-format``."""
-
-    clang_format_binary: str = "clang-format"
-    """Path to the clang-format executable"""
-
-    def indent(self, s: str):
-        prefix = " " * self.indent_width
-        return indent(s, prefix)
-
-
-class SfgOutputMode(Enum):
-    STANDALONE = auto()
-    """Generate a header/implementation file pair (e.g. ``.hpp/.cpp``) where the implementation file will
-    be compiled to a standalone object."""
-
-    INLINE = auto()
-    """Generate a header/inline implementation file pair (e.g. ``.hpp/.ipp``) where all implementations
-    are inlined by including the implementation file at the end of the header file."""
-
-    HEADER_ONLY = auto()
-    """Generate only a header file.
-
-    At the moment, header-only mode does not support generation of kernels and requires that all functions
-    and methods are marked `inline`.
-    """
-
-
-HEADER_FILE_EXTENSIONS = {"h", "hpp", "cuh"}
-IMPL_FILE_EXTENSIONS: dict[SfgOutputMode, set[str]] = {
-    SfgOutputMode.STANDALONE: {"c", "cpp", "cu"},
-    SfgOutputMode.INLINE: {".impl.h", "ipp"},
-    SfgOutputMode.HEADER_ONLY: set(),
-}
-
-
-@dataclass
-class SfgOutputSpec:
-    """Name and path specification for files output by the code generator.
-
-    Filenames are constructed as `<output_directory>/<basename>.<extension>`."""
-
-    output_directory: str
-    """Directory to which the generated files should be written."""
-
-    basename: str
-    """Base name for output files."""
-
-    header_extension: str
-    """File extension for generated header file."""
-
-    impl_extension: str
-    """File extension for generated implementation file."""
-
-    def get_header_filename(self):
-        return f"{self.basename}.{self.header_extension}"
-
-    def get_impl_filename(self):
-        return f"{self.basename}.{self.impl_extension}"
-
-    def get_header_filepath(self):
-        return path.join(self.output_directory, self.get_header_filename())
-
-    def get_impl_filepath(self):
-        return path.join(self.output_directory, self.get_impl_filename())
-
-
-@dataclass
-class SfgConfiguration:
-    """
-    Configuration for the `SfgSourceFileGenerator`.
-
-    The source file generator draws configuration from a total of four sources:
-
-    - The default configuration (`pystencilssfg.configuration.DEFAULT_CONFIG`);
-    - The project configuration;
-    - Command-line arguments;
-    - The user configuration passed to the constructor of `SourceFileGenerator`.
-
-    They take precedence in the following way:
-
-    - Project configuration overrides the default configuration
-    - Command line arguments override the project configuration
-    - User configuration overrides default and project configuration,
-      and must not conflict with command-line arguments; otherwise, an error is thrown.
-
-    **Project Configuration via Configurator Script**
-
-    Currently, the only way to define the project configuration is via a configuration module.
-    A configurator module is a Python file defining the following function at the top-level:
-
-    .. code-block:: Python
-
-        from pystencilssfg import SfgConfiguration
-
-        def sfg_config() -> SfgConfiguration:
-            # ...
-            return SfgConfiguration(
-                # ...
-            )
-
-    The configuration module is passed to the code generation script via the command-line argument
-    `--sfg-config-module`.
-    """
-
-    config_source: InitVar[SfgConfigSource | None] = None
-
-    header_extension: str | None = None
-    """File extension for generated header file."""
-
-    impl_extension: str | None = None
-    """File extension for generated implementation file."""
-
-    output_mode: SfgOutputMode | None = None
-    """The generator's output mode; defines which files to generate, and the set of legal file extensions."""
-
-    outer_namespace: str | None = None
-    """The outermost namespace in the generated file. May be a valid C++ nested namespace qualifier
-    (like ``a::b::c``) or `None` if no outer namespace should be generated."""
-
-    codestyle: SfgCodeStyle | None = None
-    """Code style that should be used by the code generator."""
-
-    output_directory: str | None = None
-    """Directory to which the generated files should be written."""
-
-    project_info: Any = None
-    """Object for managing project-specific information. To be set by the configurator script."""
-
-    def __post_init__(self, cfg_src: SfgConfigSource | None = None):
-        if self.header_extension and self.header_extension[0] == ".":
-            self.header_extension = self.header_extension[1:]
-
-        if self.impl_extension and self.impl_extension[0] == ".":
-            self.impl_extension = self.impl_extension[1:]
-
-    def override(self, other: SfgConfiguration):
-        other_dict: dict[str, Any] = {
-            k: v for k, v in _shallow_dict(other).items() if v is not None
-        }
-        return replace(self, **other_dict)
-
-    def get_output_spec(self, basename: str) -> SfgOutputSpec:
-        assert self.header_extension is not None
-        assert self.impl_extension is not None
-        assert self.output_directory is not None
-
-        return SfgOutputSpec(
-            self.output_directory, basename, self.header_extension, self.impl_extension
-        )
-
-
-DEFAULT_CONFIG = SfgConfiguration(
-    config_source=SfgConfigSource.DEFAULT,
-    header_extension="h",
-    impl_extension="cpp",
-    output_mode=SfgOutputMode.STANDALONE,
-    outer_namespace=None,
-    codestyle=SfgCodeStyle(),
-    output_directory=".",
-)
-"""Default configuration for the `SourceFileGenerator`."""
-
-
-def run_configurator(configurator_script: str):
-    cfg_modulename = path.splitext(path.split(configurator_script)[1])[0]
-
-    cfg_spec = iutil.spec_from_file_location(cfg_modulename, configurator_script)
-
-    if cfg_spec is None:
-        raise SfgConfigException(
-            SfgConfigSource.PROJECT,
-            f"Unable to load configurator script {configurator_script}",
-        )
-
-    configurator = iutil.module_from_spec(cfg_spec)
-    cfg_spec.loader.exec_module(configurator)
-
-    if not hasattr(configurator, "sfg_config"):
-        raise SfgConfigException(
-            SfgConfigSource.PROJECT,
-            "Project configurator does not define function `sfg_config`.",
-        )
-
-    project_config = configurator.sfg_config()
-    if not isinstance(project_config, SfgConfiguration):
-        raise SfgConfigException(
-            SfgConfigSource.PROJECT,
-            "sfg_config did not return a SfgConfiguration object.",
-        )
-
-    return project_config
-
-
-def add_config_args_to_parser(parser: ArgumentParser):
-    config_group = parser.add_argument_group("Configuration")
-
-    config_group.add_argument(
-        "--sfg-output-dir", type=str, default=None, dest="output_directory"
-    )
-    config_group.add_argument(
-        "--sfg-file-extensions",
-        type=str,
-        default=None,
-        dest="file_extensions",
-        help="Comma-separated list of file extensions",
-    )
-    config_group.add_argument(
-        "--sfg-output-mode",
-        type=str,
-        default=None,
-        choices=("standalone", "inline", "header-only"),
-        dest="output_mode",
-    )
-    config_group.add_argument(
-        "--sfg-config-module", type=str, default=None, dest="configurator_script"
-    )
-
-    return parser
-
-
-def config_from_parser_args(args):
-    if args.configurator_script is not None:
-        project_config = run_configurator(args.configurator_script)
-    else:
-        project_config = None
-
-    if args.output_mode is not None:
-        match args.output_mode.lower():
-            case "standalone":
-                output_mode = SfgOutputMode.STANDALONE
-            case "inline":
-                output_mode = SfgOutputMode.INLINE
-            case "header-only":
-                output_mode = SfgOutputMode.HEADER_ONLY
-            case _:
-                assert False, "invalid output mode"
-    else:
-        output_mode = None
-
-    if args.file_extensions is not None:
-        file_extentions = list(args.file_extensions.split(","))
-        h_ext, src_ext = _get_file_extensions(
-            SfgConfigSource.COMMANDLINE, file_extentions, output_mode
-        )
-    else:
-        h_ext, src_ext = None, None
-
-    cmdline_config = SfgConfiguration(
-        config_source=SfgConfigSource.COMMANDLINE,
-        header_extension=h_ext,
-        impl_extension=src_ext,
-        output_mode=output_mode,
-        output_directory=args.output_directory,
-    )
-
-    return project_config, cmdline_config
-
-
-def config_from_commandline(argv: list[str]):
-    parser = ArgumentParser(
-        "pystencilssfg",
-        description="pystencils Source File Generator",
-        allow_abbrev=False,
-    )
-
-    add_config_args_to_parser(parser)
-
-    args, script_args = parser.parse_known_args(argv)
-    project_config, cmdline_config = config_from_parser_args(args)
-
-    return project_config, cmdline_config, script_args
-
-
-def merge_configurations(
-    project_config: SfgConfiguration | None,
-    cmdline_config: SfgConfiguration | None,
-    script_config: SfgConfiguration | None,
-):
-    #   Project config completely overrides default config
-    config = DEFAULT_CONFIG
-
-    if project_config is not None:
-        config = config.override(project_config)
-
-    if cmdline_config is not None:
-        cmdline_dict = _shallow_dict(cmdline_config)
-        #   Commandline config completely overrides project and default config
-        config = config.override(cmdline_config)
-    else:
-        cmdline_dict = {}
-
-    if script_config is not None:
-        #   User config may only set values not specified on the command line
-        script_dict = _shallow_dict(script_config)
-        for key, cmdline_value in cmdline_dict.items():
-            if cmdline_value is not None and script_dict[key] is not None:
-                raise SfgException(
-                    "Conflicting configuration:"
-                    + f" Parameter {key} was specified both in the script and on the command line."
-                )
-
-        config = config.override(script_config)
-
-    return config
-
-
-def _get_file_extensions(
-    cfgsrc: SfgConfigSource, extensions: Sequence[str], output_mode: SfgOutputMode
-):
-    h_ext = None
-    src_ext = None
-
-    extensions = tuple((ext[1:] if ext[0] == "." else ext) for ext in extensions)
-
-    for ext in extensions:
-        if ext in HEADER_FILE_EXTENSIONS:
-            if h_ext is not None:
-                raise SfgConfigException(
-                    cfgsrc, "Multiple header file extensions specified."
-                )
-            h_ext = ext
-        elif ext in IMPL_FILE_EXTENSIONS[output_mode]:
-            if src_ext is not None:
-                raise SfgConfigException(
-                    cfgsrc, "Multiple source file extensions specified."
-                )
-            src_ext = ext
-        else:
-            raise SfgConfigException(
-                cfgsrc, f"Invalid file extension '.{ext}' for output mode {output_mode}"
-            )
-
-    return h_ext, src_ext
-
-
-def _shallow_dict(obj):
-    """Workaround to create a shallow dict of a dataclass object, see
-    https://docs.python.org/3/library/dataclasses.html#dataclasses.asdict."""
-    return dict((field.name, getattr(obj, field.name)) for field in fields(obj))
diff --git a/src/pystencilssfg/emission/clang_format.py b/src/pystencilssfg/emission/clang_format.py
index eea152a062474a6761fdbbdecfaaeb88bb63c4d3..53540d3c4698aa1e16bb846e41f8bb7f8c87a35a 100644
--- a/src/pystencilssfg/emission/clang_format.py
+++ b/src/pystencilssfg/emission/clang_format.py
@@ -1,11 +1,11 @@
 import subprocess
 import shutil
 
-from ..configuration import SfgCodeStyle
+from ..config import ClangFormatOptions
 from ..exceptions import SfgException
 
 
-def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str:
+def invoke_clang_format(code: str, options: ClangFormatOptions) -> str:
     """Call the `clang-format` command-line tool to format the given code string
     according to the given style arguments.
 
@@ -24,13 +24,15 @@ def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str:
         be executed (binary not found, or error during exection), the function will
         throw an exception.
     """
-    if codestyle.skip_clang_format:
+    if options.get_option("skip"):
         return code
 
-    args = [codestyle.clang_format_binary, f"--style={codestyle.code_style}"]
+    binary = options.get_option("binary")
+    force = options.get_option("force")
+    args = [binary, f"--style={options.code_style}"]
 
-    if not shutil.which(codestyle.clang_format_binary):
-        if codestyle.force_clang_format:
+    if not shutil.which(binary):
+        if force:
             raise SfgException(
                 "`force_clang_format` was set to true in code style, "
                 "but clang-format binary could not be found."
@@ -41,7 +43,7 @@ def invoke_clang_format(code: str, codestyle: SfgCodeStyle) -> str:
     result = subprocess.run(args, input=code, capture_output=True, text=True)
 
     if result.returncode != 0:
-        if codestyle.force_clang_format:
+        if force:
             raise SfgException(f"Call to clang-format failed: \n{result.stderr}")
         else:
             return code
diff --git a/src/pystencilssfg/emission/emitter.py b/src/pystencilssfg/emission/emitter.py
index e96e1e187bb55fcaca892c60f751649a90e4c6a7..c32b18af351579b98477bf688c0789a394fc3732 100644
--- a/src/pystencilssfg/emission/emitter.py
+++ b/src/pystencilssfg/emission/emitter.py
@@ -1,9 +1,12 @@
+from __future__ import annotations
+
 from typing import Sequence
 from abc import ABC, abstractmethod
 from dataclasses import dataclass
 from os import path
 
 from ..context import SfgContext
+from ..config import SfgConfig, OutputMode
 
 
 @dataclass
@@ -36,6 +39,26 @@ class OutputSpec:
     def get_impl_filepath(self):
         return path.join(self.output_directory, self.get_impl_filename())
 
+    @staticmethod
+    def create(config: SfgConfig, basename: str) -> OutputSpec:
+        output_mode = config.get_option("output_mode")
+        header_extension = config.extensions.get_option("header")
+        impl_extension = config.extensions.get_option("impl")
+
+        if impl_extension is None:
+            match output_mode:
+                case OutputMode.INLINE:
+                    impl_extension = "ipp"
+                case OutputMode.STANDALONE:
+                    impl_extension = "cpp"
+
+        return OutputSpec(
+            config.get_option("output_directory"),
+            basename,
+            header_extension,
+            impl_extension,
+        )
+
 
 class AbstractEmitter(ABC):
     @property
diff --git a/src/pystencilssfg/emission/header_impl_pair.py b/src/pystencilssfg/emission/header_impl_pair.py
index 9e1e69729418240844f192777bdd609a054f3614..d70cfd255e5df6d01169eea50fe4ac87bba37bdf 100644
--- a/src/pystencilssfg/emission/header_impl_pair.py
+++ b/src/pystencilssfg/emission/header_impl_pair.py
@@ -5,6 +5,7 @@ from ..context import SfgContext
 from .prepare import prepare_context
 from .printers import SfgHeaderPrinter, SfgImplPrinter
 from .clang_format import invoke_clang_format
+from ..config import ClangFormatOptions
 
 from .emitter import AbstractEmitter, OutputSpec
 
@@ -12,7 +13,12 @@ from .emitter import AbstractEmitter, OutputSpec
 class HeaderImplPairEmitter(AbstractEmitter):
     """Emits a header-implementation file pair."""
 
-    def __init__(self, output_spec: OutputSpec, inline_impl: bool = False):
+    def __init__(
+        self,
+        output_spec: OutputSpec,
+        inline_impl: bool = False,
+        clang_format: ClangFormatOptions | None = None,
+    ):
         """Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec]."""
         self._basename = output_spec.basename
         self._output_directory = output_spec.output_directory
@@ -21,6 +27,7 @@ class HeaderImplPairEmitter(AbstractEmitter):
         self._inline_impl = inline_impl
 
         self._ospec = output_spec
+        self._clang_format = clang_format
 
     @property
     def output_files(self) -> Sequence[str]:
@@ -41,8 +48,9 @@ class HeaderImplPairEmitter(AbstractEmitter):
         header = header_printer.get_code()
         impl = impl_printer.get_code()
 
-        header = invoke_clang_format(header, ctx.codestyle)
-        impl = invoke_clang_format(impl, ctx.codestyle)
+        if self._clang_format is not None:
+            header = invoke_clang_format(header, self._clang_format)
+            impl = invoke_clang_format(impl, self._clang_format)
 
         makedirs(self._output_directory, exist_ok=True)
 
diff --git a/src/pystencilssfg/emission/header_only.py b/src/pystencilssfg/emission/header_only.py
index 169d2bc00240ac2898234aa657614e45ebe58857..ce6ce90f609f110153c5d5ae753d0d511540ab18 100644
--- a/src/pystencilssfg/emission/header_only.py
+++ b/src/pystencilssfg/emission/header_only.py
@@ -4,19 +4,23 @@ from os import path, makedirs
 from ..context import SfgContext
 from .prepare import prepare_context
 from .printers import SfgHeaderPrinter
+from ..config import ClangFormatOptions
 from .clang_format import invoke_clang_format
 
 from .emitter import AbstractEmitter, OutputSpec
 
 
 class HeaderOnlyEmitter(AbstractEmitter):
-    def __init__(self, output_spec: OutputSpec):
+    def __init__(
+        self, output_spec: OutputSpec, clang_format: ClangFormatOptions | None = None
+    ):
         """Create a `HeaderImplPairEmitter` from an [SfgOutputSpec][pystencilssfg.configuration.SfgOutputSpec]."""
         self._basename = output_spec.basename
         self._output_directory = output_spec.output_directory
         self._header_filename = output_spec.get_header_filename()
 
         self._ospec = output_spec
+        self._clang_format = clang_format
 
     @property
     def output_files(self) -> Sequence[str]:
@@ -28,7 +32,8 @@ class HeaderOnlyEmitter(AbstractEmitter):
 
         header_printer = SfgHeaderPrinter(ctx, self._ospec)
         header = header_printer.get_code()
-        header = invoke_clang_format(header, ctx.codestyle)
+        if self._clang_format is not None:
+            header = invoke_clang_format(header, self._clang_format)
 
         makedirs(self._output_directory, exist_ok=True)
 
diff --git a/src/pystencilssfg/emission/printers.py b/src/pystencilssfg/emission/printers.py
index c562bf7bca5d59365c3a1cd5b9d77b8f7e7920f5..6daccfa0c5edab18fa4c509ad8eb379a5f163204 100644
--- a/src/pystencilssfg/emission/printers.py
+++ b/src/pystencilssfg/emission/printers.py
@@ -7,7 +7,6 @@ from pystencils import KernelFunction
 from pystencils.backend.emission import emit_code
 
 from ..context import SfgContext
-from ..configuration import SfgOutputSpec
 from ..visitors import visitor
 from ..exceptions import SfgException
 
@@ -25,6 +24,8 @@ from ..ir.source_components import (
     SfgVisibilityBlock,
 )
 
+from .emitter import OutputSpec
+
 
 def interleave(*iters):
     try:
@@ -71,7 +72,7 @@ class SfgGeneralPrinter:
 
 class SfgHeaderPrinter(SfgGeneralPrinter):
     def __init__(
-        self, ctx: SfgContext, output_spec: SfgOutputSpec, inline_impl: bool = False
+        self, ctx: SfgContext, output_spec: OutputSpec, inline_impl: bool = False
     ):
         self._output_spec = output_spec
         self._ctx = ctx
@@ -180,7 +181,7 @@ class SfgHeaderPrinter(SfgGeneralPrinter):
 
 class SfgImplPrinter(SfgGeneralPrinter):
     def __init__(
-        self, ctx: SfgContext, output_spec: SfgOutputSpec, inline_impl: bool = False
+        self, ctx: SfgContext, output_spec: OutputSpec, inline_impl: bool = False
     ):
         self._output_spec = output_spec
         self._ctx = ctx
diff --git a/src/pystencilssfg/generator.py b/src/pystencilssfg/generator.py
index c2461800348c570a02b5cdc52de880d106b3a6c4..83298140d9015e48765d4a3f93750a2beae35777 100644
--- a/src/pystencilssfg/generator.py
+++ b/src/pystencilssfg/generator.py
@@ -1,4 +1,3 @@
-import sys
 import os
 from os import path
 
@@ -65,29 +64,29 @@ class SourceFileGenerator:
         self._context.add_include(SfgHeaderInclude("cstdint", system_header=True))
         self._context.add_definition("#define RESTRICT __restrict__")
 
-        output_spec = OutputSpec(
-            config.get_option("output_directory"),
-            basename,
-            config.extensions.get_option("header"),
-            config.extensions.get_option("impl"),
-        )
+        output_mode = config.get_option("output_mode")
+        output_spec = OutputSpec.create(config, basename)
 
         self._emitter: AbstractEmitter
-        match config.get_option("output_mode"):
+        match output_mode:
             case OutputMode.HEADER_ONLY:
                 from .emission import HeaderOnlyEmitter
 
-                self._emitter = HeaderOnlyEmitter(output_spec)
+                self._emitter = HeaderOnlyEmitter(
+                    output_spec, clang_format=config.clang_format
+                )
             case OutputMode.INLINE:
                 from .emission import HeaderImplPairEmitter
 
                 self._emitter = HeaderImplPairEmitter(
-                    output_spec, inline_impl=True
+                    output_spec, inline_impl=True, clang_format=config.clang_format
                 )
             case OutputMode.STANDALONE:
                 from .emission import HeaderImplPairEmitter
 
-                self._emitter = HeaderImplPairEmitter(output_spec)
+                self._emitter = HeaderImplPairEmitter(
+                    output_spec, clang_format=config.clang_format
+                )
 
     def clean_files(self):
         for file in self._emitter.output_files:
diff --git a/tests/generator/data/project_config.py b/tests/generator/data/project_config.py
index 73e5e9c6d52d979542b1f31137f8f31cf5e0c637..e4e8412a5f842d06f0f9270c5417279f9d5e4bac 100644
--- a/tests/generator/data/project_config.py
+++ b/tests/generator/data/project_config.py
@@ -2,8 +2,9 @@ from pystencilssfg import SfgConfig
 
 
 def configure_sfg(cfg: SfgConfig):
-    cfg.codestyle.code_style = "llvm"
     cfg.codestyle.indent_width = 3
+    cfg.clang_format.code_style = "llvm"
+    cfg.clang_format.force = True
     cfg.output_directory = "generated_sources"
     cfg.outer_namespace = "myproject"
     cfg.extensions.header = "hpp"
@@ -11,3 +12,11 @@ def configure_sfg(cfg: SfgConfig):
 
 magic_string = "Spam and eggs"
 magic_number = 0xcafe
+
+
+def project_info():
+    return {
+        "use_openmp": True,
+        "use_cuda": True,
+        "float_format": "float32"
+    }
diff --git a/tests/generator/test_config.py b/tests/generator/test_config.py
index 879d47f71934c87d120a71cbb856f036e00ecbee..66b0a9f75bf8bc997e8dca04b2be9f8e454604ca 100644
--- a/tests/generator/test_config.py
+++ b/tests/generator/test_config.py
@@ -18,6 +18,8 @@ def test_defaults():
     assert cfg.get_option("output_mode") == OutputMode.STANDALONE
     assert cfg.extensions.get_option("header") == "hpp"
     assert cfg.codestyle.get_option("indent_width") == 2
+    assert cfg.clang_format.get_option("binary") == "clang-format"
+    assert cfg.clang_format.get_option("code_style") == "file"
     assert cfg.get_option("outer_namespace") is GLOBAL_NAMESPACE
 
     cfg.extensions.impl = ".cu"
@@ -25,10 +27,10 @@ def test_defaults():
 
     #   Check that section subobjects of different config objects are independent
     #   -> must use default_factory to construct them, because they are mutable!
-    cfg.codestyle.clang_format_binary = "bogus"
+    cfg.clang_format.binary = "bogus"
 
     cfg2 = SfgConfig()
-    assert cfg2.codestyle.clang_format_binary is None
+    assert cfg2.clang_format.binary is None
 
 
 def test_override():
@@ -36,23 +38,23 @@ def test_override():
     cfg1.outer_namespace = "test"
     cfg1.extensions.header = "h"
     cfg1.extensions.impl = "c"
-    cfg1.codestyle.force_clang_format = True
+    cfg1.clang_format.force = True
 
     cfg2 = SfgConfig()
     cfg2.outer_namespace = GLOBAL_NAMESPACE
     cfg2.extensions.header = "hpp"
     cfg2.extensions.impl = "cpp"
-    cfg2.codestyle.clang_format_binary = "bogus"
+    cfg2.clang_format.binary = "bogus"
 
     cfg1.override(cfg2)
 
     assert cfg1.outer_namespace is GLOBAL_NAMESPACE
     assert cfg1.extensions.header == "hpp"
     assert cfg1.extensions.impl == "cpp"
-    assert cfg1.codestyle.force_clang_format is True
     assert cfg1.codestyle.indent_width is None
-    assert cfg1.codestyle.code_style is None
-    assert cfg1.codestyle.clang_format_binary == "bogus"
+    assert cfg1.clang_format.force is True
+    assert cfg1.clang_format.code_style is None
+    assert cfg1.clang_format.binary == "bogus"
 
 
 def test_from_commandline():
@@ -79,8 +81,9 @@ def test_from_commandline():
     cli_args = CommandLineParameters(args)
     cfg = cli_args.get_config()
 
-    assert cfg.codestyle.code_style == "llvm"
     assert cfg.codestyle.indent_width == 3
+    assert cfg.clang_format.code_style == "llvm"
+    assert cfg.clang_format.force is True
     assert (
         cfg.output_directory == "gen_sources"
     )  # value from config module overridden by commandline
@@ -114,3 +117,9 @@ def test_generator_config(monkeypatch, tmp_path):
 
         assert ctx.outer_namespace == "myproject"
         assert ctx.argv == ["test1", "test2"]
+        assert isinstance(ctx.project_info, dict)
+        assert ctx.project_info == {
+            "use_openmp": True,
+            "use_cuda": True,
+            "float_format": "float32",
+        }
diff --git a/tests/generator_scripts/scripts/Structural.py b/tests/generator_scripts/scripts/Structural.py
index e8cf2ab0d87ecf770871c6ef2d15deb7820b4561..7cfe352910b676429b97cb8f3d29bec68b74810a 100644
--- a/tests/generator_scripts/scripts/Structural.py
+++ b/tests/generator_scripts/scripts/Structural.py
@@ -1,9 +1,8 @@
-from pystencilssfg import SourceFileGenerator, SfgConfiguration, SfgCodeStyle
+from pystencilssfg import SourceFileGenerator, SfgConfig
 
 #   Do not use clang-format, since it reorders headers
-cfg = SfgConfiguration(
-    codestyle=SfgCodeStyle(skip_clang_format=True)
-)
+cfg = SfgConfig()
+cfg.clang_format.skip = True
 
 with SourceFileGenerator(cfg) as sfg:
     sfg.prelude("Expect the unexpected, and you shall never be surprised.")
diff --git a/tests/generator_scripts/test_generator_scripts.py b/tests/generator_scripts/test_generator_scripts.py
index 51e05c01f2cae3a637e235e4a94d1585b390a5af..d3173dce73f14458c02ad400b73f4d52d21cf947 100644
--- a/tests/generator_scripts/test_generator_scripts.py
+++ b/tests/generator_scripts/test_generator_scripts.py
@@ -50,10 +50,10 @@ When adding new generator scripts to the `scripts` directory,
 do not forget to include them here.
 """
 SCRIPTS = [
-    ScriptInfo.make("Structural", ("h", "cpp")),
-    ScriptInfo.make("SimpleJacobi", ("h", "cpp"), compilable_output="cpp"),
-    ScriptInfo.make("SimpleClasses", ("h", "cpp")),
-    ScriptInfo.make("Variables", ("h", "cpp"), compilable_output="cpp"),
+    ScriptInfo.make("Structural", ("hpp", "cpp")),
+    ScriptInfo.make("SimpleJacobi", ("hpp", "cpp"), compilable_output="cpp"),
+    ScriptInfo.make("SimpleClasses", ("hpp", "cpp")),
+    ScriptInfo.make("Variables", ("hpp", "cpp"), compilable_output="cpp"),
 ]