diff --git a/CHANGELOG.md b/CHANGELOG.md
index b3e58f7319342388df2ee6b697aad1683bb5a3c9..687c4c4e355722313bfe6832916c40b8b4963e37 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
 
 ## Version 0.8.0 - Released 2024-08-14
 
+### Add
+* Add experimental new flag `__allow_newattr__` which relaxes the constraint
+  that you can't add keys on the fly.
+
 ### Removed
 
 * Remove 3.6 and 3.7 support
diff --git a/scriptconfig/config.py b/scriptconfig/config.py
index a71c4d6842decbe905c5d28b775e6b89c1ff6956..be32cdf92ff3fe051b5ecdff5a8782627e9f7fb5 100644
--- a/scriptconfig/config.py
+++ b/scriptconfig/config.py
@@ -308,6 +308,7 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
     """
     __scfg_class__ = 'Config'
     __default__ = {}
+    # __allow_newattr__ = False
 
     def __init__(self, data=None, default=None, cmdline=False,
                  _dont_call_post_init=False):
@@ -525,13 +526,17 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
         if key not in self._data:
             key = self._normalize_alias_key(key)
             if key not in self._data:
-                raise Exception('Cannot add keys to ScriptConfig objects')
+                if not getattr(self, '__allow_newattr__', False):
+                    raise Exception(
+                        'Cannot add keys to scriptconfig.Config objects unless '
+                        'self.__allow_newattr__ is True'
+                    )
         if scfg_isinstance(value, Value):
             # If the new item is a Value object simply overwrite the old one
             self._data[key] = value
         else:
-            template = self.__default__[key]
-            if scfg_isinstance(template, Value):
+            template = self.__default__.get(key, None)
+            if template is not None and scfg_isinstance(template, Value):
                 # If the new value is raw data, and we have a underlying Value
                 # object update it.
                 self._data[key] = template.cast(value)
@@ -788,7 +793,7 @@ class Config(ub.NiceRepr, DictLike, metaclass=MetaConfig):
         """
         if getattr(self, '_alias_map', None) is None:
             self._alias_map = self._build_alias_map()
-        return self._alias_map[key]
+        return self._alias_map.get(key, key)
 
     def _normalize_alias_dict(self, data):
         """
diff --git a/scriptconfig/dataconfig.py b/scriptconfig/dataconfig.py
index 4e423676e9aa7e7f0059a6fc4e4e5bfd185c3615..3941c5735cd58d1dd4df5919005c33159d934588 100644
--- a/scriptconfig/dataconfig.py
+++ b/scriptconfig/dataconfig.py
@@ -337,16 +337,23 @@ class DataConfig(Config, metaclass=MetaDataConfig):
         Forwards setattrs in the configuration to the dictionary interface,
         otherwise passes it through.
         """
-        if not key.startswith('_') and getattr(self, '_enable_setattr', False) and key in self:
-            # After object initialization allow the user to use setattr on any
-            # value in the underlying dictionary. Everything else uses the
-            # normal mechanism.
-            try:
-                self[key] = value
-            except KeyError:
-                raise AttributeError(key)
-        else:
+        if key.startswith('_'):
+            # Currently we do not allow leading underscores to be config
+            # values to give us some flexibility for API changes.
             self.__dict__[key] = value
+        else:
+            can_setattr = (getattr(self, '__allow_newattr__', False))  # case where user can add new keys on the fly
+            can_setattr |= (getattr(self, '_enable_setattr', False) and key in self)  # internal usage for initialization
+            if can_setattr:
+                # After object initialization allow the user to use setattr on any
+                # value in the underlying dictionary. Everything else uses the
+                # normal mechanism.
+                try:
+                    self[key] = value
+                except KeyError:
+                    raise AttributeError(key)
+            else:
+                self.__dict__[key] = value
 
     @classmethod
     def legacy(cls, cmdline=False, data=None, default=None, strict=False):
diff --git a/tests/test_newattr.py b/tests/test_newattr.py
new file mode 100644
index 0000000000000000000000000000000000000000..048a9d0c940f0f3bb6407420d408cf6232d7c15a
--- /dev/null
+++ b/tests/test_newattr.py
@@ -0,0 +1,47 @@
+def test_newattr():
+    """
+    Create an empty config and test different ways of adding new attributes.
+    Test allowed and disallowed cases.
+    """
+
+    import scriptconfig as scfg
+    import pytest
+    class TestNewattrCLI(scfg.DataConfig):
+        ...
+
+    config = TestNewattrCLI()
+
+    # By default new attributes are not allowed via the dictionary interface
+    with pytest.raises(Exception):
+        config['newattr1'] = 123
+
+    # Quirk: they are allowed via setattr, but they do not become part of the
+    # config. This is something we could change.
+    config.newattr2 = 456
+    assert 'newattr2' in config.__dict__
+    assert 'newattr2' not in config
+    assert config.newattr2 == 456, (
+        'even though it is not in the config, you can still access it '
+        'to cary info around')
+    print(f'config={config}')
+    print(f'config.__dict__={config.__dict__}')
+
+    config = TestNewattrCLI()
+    # Enable experimental newattr
+    config.__allow_newattr__ = True
+
+    config['newattr1'] = 123
+    config.newattr2 = 456
+
+    assert 'newattr2' in config
+    assert 'newattr1' in config
+    print(f'config={config}')
+
+
+if __name__ == '__main__':
+    """
+
+    CommandLine:
+        python ~/code/scriptconfig/tests/test_newattr.py
+    """
+    test_newattr()