diff --git a/config/install/config_readonly.configreadonlyexceptions.yml b/config/install/config_readonly.configreadonlyexceptions.yml
new file mode 100644
index 0000000..5f3cdc7
--- /dev/null
+++ b/config/install/config_readonly.configreadonlyexceptions.yml
@@ -0,0 +1 @@
+config_readonly:
diff --git a/config_readonly.links.menu.yml b/config_readonly.links.menu.yml
new file mode 100644
index 0000000..78c0ec4
--- /dev/null
+++ b/config_readonly.links.menu.yml
@@ -0,0 +1,7 @@
+config_readonly.config_readonly_exceptions_form:
+  title: 'Config Readonly Exceptions'
+  route_name: config_readonly.config_readonly_exceptions_form
+  description: 'Manage exceptions for the Config Readonly module'
+  parent: system.admin_config_system
+  weight: 99
+
diff --git a/config_readonly.module b/config_readonly.module
index b8d8cb7..415cbe5 100644
--- a/config_readonly.module
+++ b/config_readonly.module
@@ -1,5 +1,10 @@
 <?php
 
+/**
+ * @file
+ * Config read only module.
+ */
+
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Site\Settings;
 use Drupal\config_readonly\ReadOnlyFormEvent;
diff --git a/config_readonly.routing.yml b/config_readonly.routing.yml
new file mode 100644
index 0000000..a336eff
--- /dev/null
+++ b/config_readonly.routing.yml
@@ -0,0 +1,10 @@
+
+config_readonly.config_readonly_exceptions_form:
+  path: '/admin/config/config_readonly/exceptions'
+  defaults:
+    _form: '\Drupal\config_readonly\Form\ConfigReadonlyExceptionsForm'
+    _title: 'Config Readonly Exceptions'
+  requirements:
+    _permission: 'access administration pages'
+  options:
+    _admin_route: TRUE
diff --git a/config_readonly.services.yml b/config_readonly.services.yml
index 8d15c34..29428fb 100644
--- a/config_readonly.services.yml
+++ b/config_readonly.services.yml
@@ -3,3 +3,6 @@ services:
     class: Drupal\config_readonly\EventSubscriber\ReadOnlyFormSubscriber
     tags:
       - { name: event_subscriber }
+  config_readonly_exceptions:
+    class: Drupal\config_readonly\ConfigReadonlyExceptionsServiceProvider
+    arguments: ["@serialization.yaml", "@config.factory"]
diff --git a/src/Config/ConfigReadonlyStorage.php b/src/Config/ConfigReadonlyStorage.php
index 77a2c9f..c5dd704 100644
--- a/src/Config/ConfigReadonlyStorage.php
+++ b/src/Config/ConfigReadonlyStorage.php
@@ -8,6 +8,7 @@ use Drupal\Core\Config\StorageInterface;
 use Drupal\Core\Cache\CacheBackendInterface;
 use Drupal\Core\Lock\LockBackendInterface;
 use Drupal\Core\Site\Settings;
+use Drupal\config_readonly\ConfigReadonlyExceptionsServiceProvider;
 
 use Symfony\Cmf\Component\Routing\RouteObjectInterface;
 use Symfony\Component\HttpFoundation\RequestStack;
@@ -68,7 +69,7 @@ class ConfigReadonlyStorage extends CachedStorage {
    * @throws Exception
    */
   public function write($name, array $data) {
-    $this->checkLock();
+    $this->checkLock($name);
     return parent::write($name, $data);
   }
 
@@ -78,7 +79,7 @@ class ConfigReadonlyStorage extends CachedStorage {
    * @throws Exception
    */
   public function delete($name) {
-    $this->checkLock();
+    $this->checkLock($name);
     return parent::delete($name);
   }
 
@@ -98,11 +99,17 @@ class ConfigReadonlyStorage extends CachedStorage {
    * @throws Exception
    */
   public function deleteAll($prefix = '') {
-    $this->checkLock();
+    $this->checkLock($name);
     return parent::deleteAll($prefix);
   }
 
-  protected function checkLock() {
+  /**
+   * CHeck whether config is currently locked.
+   *
+   * @param string $name
+   *   Check for a specific lock name.
+   */
+  protected function checkLock($name = '') {
     // If settings.php says to lock config changes and if the config importer
     // isn't running (we do not want to lock config imports), then throw an
     // exception.
@@ -110,11 +117,18 @@ class ConfigReadonlyStorage extends CachedStorage {
     if (Settings::get('config_readonly') && $this->lock->lockMayBeAvailable(ConfigImporter::LOCK_NAME)) {
       $request = $this->requestStack->getCurrentRequest();
       if ($request && $request->attributes->get(RouteObjectInterface::ROUTE_NAME) === 'system.db_update') {
-        // We seem to be in the middle of running update.php
+        // We seem to be in the middle of running update.php.
         // @see \Drupal\Core\Update\UpdateKernel::setupRequestMatch()
         // @todo - always allow or support a flag for blocking it?
         return;
       }
+
+      // Don't block particular patterns.
+      $exceptions = \Drupal::service('config_readonly_exceptions');
+      if ($exceptions->matchesException($name)) {
+        return;
+      }
+
       throw new \Exception('Your site configuration active store is currently locked.');
     }
   }
diff --git a/src/ConfigReadonlyExceptionsServiceProvider.php b/src/ConfigReadonlyExceptionsServiceProvider.php
new file mode 100644
index 0000000..f91ae97
--- /dev/null
+++ b/src/ConfigReadonlyExceptionsServiceProvider.php
@@ -0,0 +1,260 @@
+<?php
+
+namespace Drupal\config_readonly;
+
+use Drupal\Component\Serialization\Yaml;
+use Drupal\Core\Config\ConfigFactoryInterface;
+
+/**
+ * Class ConfigReadonlyExceptionsServiceProvider.
+ *
+ * @package Drupal\config_readonly
+ */
+class ConfigReadonlyExceptionsServiceProvider {
+
+  /**
+   * Drupal\Core\Config\ConfigFactoryInterface definition.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * Drupal\Component\Serialization\Yaml definition.
+   *
+   * @var \Drupal\Component\Serialization\Yaml
+   */
+  protected $yaml;
+
+  /**
+   * An array to store the active config exceptions settings.
+   *
+   * @var array
+   */
+  protected $config;
+
+  /**
+   * Whether or not to throw exceptions.
+   *
+   * @var boolean
+   */
+  protected $throwExceptions = TRUE;
+
+  /**
+   * Store the errors here in case we aren't throwing exceptions.
+   *
+   * @var array
+   */
+  protected $errors = [];
+
+  /**
+   * Constructs a \Drupal\system\ConfigFormBase object.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   */
+  public function __construct(Yaml $serialization_yaml, ConfigFactoryInterface $config_factory) {
+    $this->configFactory = $config_factory;
+    $this->yaml = $serialization_yaml;
+    $this->config = $this->configFactory->get('config_readonly.config_readonly_exceptions');
+  }
+
+  /**
+   * Whether or not to throw exceptions. Defaults to true.
+   *
+   * In validation we don't want to throw exceptions, we instead want to show
+   * validation errors.
+   *
+   * @param bool $bool
+   *   Whether or not throw exceptions.
+   */
+  public function throwExceptions($bool) {
+    $this->throwExceptions = ($bool ? TRUE : FALSE);
+  }
+
+  /**
+   * Get the errors.
+   *
+   * If we aren't throwing exceptions, we may want to retrieve any errors
+   * in for instance parsing the config ignore file.
+   *
+   * @return array
+   *   An array of errors.
+   */
+  public function getErrors() {
+    return $this->errors;
+  }
+
+  /**
+   * Set an error or throw exception.
+   *
+   * @param string $error
+   *   The error.
+   */
+  protected function setError($error) {
+    $this->errors[] = $error;
+    if ($this->throwExceptions) {
+      throw new ConfigReadonlyDrushParseException($error);
+    }
+  }
+
+  /**
+   * Get exceptions.
+   *
+   * @return array
+   *   An array of exceptions.
+   */
+  public function getExceptionPatterns() {
+    $patterns = [];
+    $exceptions_type = $this->config->get('exceptions_type');
+    switch ($exceptions_type) {
+      case 'from_manual_config':
+        $patterns = $this->getPatternsFromManualConfig();
+        break;
+
+      case 'from_drush_config_ignore':
+        $patterns = $this->getPatternsFromDrushConfigIgnore();
+        break;
+
+    }
+    return $patterns;
+  }
+
+  /**
+   * Get patterns from config ignore exceptions.
+   *
+   * @param string $config_ignore_path
+   *   Potentially specify a path to test otherwise use the path specified
+   *   in the config settings.
+   *
+   * @return array
+   *   The patterns.
+   */
+  public function getExceptionsFromDrushConfigIgnore($config_ignore_path = '') {
+    if (!$config_ignore_path) {
+      $config_ignore_path = $this->config->get('config_ignore_path');
+    }
+    if ($ignore_file = $config_ignore_path) {
+      if (!is_file($ignore_file)) {
+        $this->setError('The drush config ignore file does not appear to exist.');
+        return [];
+      }
+      if ($string = file_get_contents($ignore_file)) {
+        $parsed = FALSE;
+        try {
+          $parsed = $this->yaml::decode($string);
+        }
+        catch (InvalidDataTypeException $e) {
+          $this->setError('Unable to parse the drush config ignore file.');
+          return [];
+        }
+        if (!isset($parsed['ignore']) || !is_array($parsed['ignore'])) {
+          $this->setError('The drush config ignore file does not contain a valid ignore array.');
+          return [];
+        }
+        return $parsed['ignore'];
+      }
+    }
+    return [];
+  }
+
+  /**
+   * Get exceptions from Drush config-ignore file.
+   *
+   * Based off the code from PreviousNext drush cexy / cimy to ensure
+   * consistency of result.
+   *
+   * @return array
+   *   The patterns.
+   *
+   * @link https://www.previousnext.com.au/blog/introducing-drush-cmi-tools
+   *   Drush CMI Tools from PreviousNext.
+   */
+  public function getPatternsFromDrushConfigIgnore() {
+    $patterns = [];
+    $exceptions = $this->getExceptionsFromDrushConfigIgnore();
+    if ($exceptions) {
+      foreach ($exceptions as $exception) {
+        $pattern = $this->getSinglePattern($exception);
+        if ($pattern) {
+          $patterns[] = $pattern;
+        }
+      }
+    }
+    return $patterns;
+  }
+
+  /**
+   * Get exceptions from manual config settings.
+   *
+   * @return array
+   *   The patterns.
+   */
+  protected function getPatternsFromManualConfig() {
+    $patterns = [];
+    $exceptions = $this->config->get('exceptions');
+    if ($exceptions) {
+      foreach ($exceptions as $exception) {
+        $pattern = $this->getSinglePattern($exception);
+        if ($pattern) {
+          $patterns[] = $pattern;
+        }
+      }
+    }
+    return $patterns;
+  }
+
+  /**
+   * Get single pattern from an exception.
+   *
+   * Based off the code from PreviousNext drush cexy / cimy to ensure
+   * consistency of result.
+   *
+   * @param string $exception
+   *   The raw exception potentially with wildcard.
+   *
+   * @return string
+   *   The pattern.
+   *
+   * @link https://www.previousnext.com.au/blog/introducing-drush-cmi-tools
+   *   Drush CMI Tools from PreviousNext.
+   */
+  protected function getSinglePattern($exception) {
+    // Allow for accidental .yml extension.
+    if (substr($exception, -4) === '.yml') {
+      $exception = substr($exception, 0, -4);
+    }
+    return '/' . str_replace('\*', '(.*)', preg_quote($exception)) . '\.yml/';
+  }
+
+  /**
+   * Check if the given name matches any exception.
+   *
+   * @param string $name
+   *   The config name.
+   *
+   * @return bool
+   *   Whether or not there is a match.
+   */
+  public function matchesException($name) {
+    // Allow for accidental .yml extension.
+    if (substr($name, -4) !== '.yml') {
+      $name .= '.yml';
+    }
+
+    // Check for matches.
+    $patterns = $this->getExceptionPatterns();
+    dpm($patterns);
+    dpm($name);
+    if ($patterns) {
+      foreach ($patterns as $pattern) {
+        dpm($pattern . ' : ' . (preg_match($pattern, $name) ? 'yes' : 'no'));
+        if (preg_match($pattern, $name)) {
+          return TRUE;
+        }
+      }
+    }
+    return FALSE;
+  }
+
+}
diff --git a/src/ConfigReadonlyServiceProvider.php b/src/ConfigReadonlyServiceProvider.php
index cb0d277..dfb51a6 100644
--- a/src/ConfigReadonlyServiceProvider.php
+++ b/src/ConfigReadonlyServiceProvider.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\config_readonly\ConfigReadonlyServiceProvider.
- */
-
 namespace Drupal\config_readonly;
 
 use Symfony\Component\DependencyInjection\Reference;
@@ -30,8 +25,14 @@ class ConfigReadonlyServiceProvider implements ServiceProviderInterface, Service
     if ($container->getParameter('kernel.environment') !== 'install') {
       $definition = $container->getDefinition('config.storage');
       $definition->setClass('Drupal\config_readonly\Config\ConfigReadonlyStorage');
-      $definition->setArguments([new Reference('config.storage.active'), new Reference('cache.config'), new Reference('lock'), new Reference('request_stack')]);
+      $definition->setArguments([
+        new Reference('config.storage.active'),
+        new Reference('cache.config'),
+        new Reference('lock'),
+        new Reference('request_stack'),
+        new Reference('request_stack'),
+      ]);
     }
   }
-}
 
+}
diff --git a/src/EventSubscriber/ReadOnlyFormSubscriber.php b/src/EventSubscriber/ReadOnlyFormSubscriber.php
index 13d7582..947a66f 100644
--- a/src/EventSubscriber/ReadOnlyFormSubscriber.php
+++ b/src/EventSubscriber/ReadOnlyFormSubscriber.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\config_readonly\EventSubscriber\ReadOnlyFormSubscriber.
- */
-
 namespace Drupal\config_readonly\EventSubscriber;
 
 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@@ -41,12 +36,24 @@ class ReadOnlyFormSubscriber implements EventSubscriberInterface {
       $mark_form_read_only = in_array($form_object->getFormId(), $this->readOnlyFormIds);
     }
 
-    // Check if the form is an EntityFormInterface and entity is a config entity.
+    // Check if the form is an EntityFormInterface and entity is a config
+    // entity.
     if (!$mark_form_read_only && $form_object instanceof EntityFormInterface) {
       $entity = $form_object->getEntity();
       $mark_form_read_only = $entity instanceof ConfigEntityInterface;
     }
 
+    // Don't block particular patterns.
+    $exceptions = \Drupal::service('config_readonly_exceptions');
+
+    if ($form_object instanceof EntityFormInterface) {
+      $entity = $form_object->getEntity();
+      $name = $entity->getConfigTarget();
+      if ($exceptions->matchesException($name)) {
+        $mark_form_read_only = FALSE;
+      }
+    }
+
     if ($mark_form_read_only) {
       $event->markFormReadOnly();
     }
diff --git a/src/Exception/ConfigReadonlyDrushParseException.php b/src/Exception/ConfigReadonlyDrushParseException.php
new file mode 100644
index 0000000..e630f45
--- /dev/null
+++ b/src/Exception/ConfigReadonlyDrushParseException.php
@@ -0,0 +1,8 @@
+<?php
+
+namespace Drupal\Core\Entity\Exception;
+
+/**
+ * Defines an exception thrown when unable to parse the drush config-ignore.yml.
+ */
+class ConfigReadonlyDrushParseException extends \Exception {}
diff --git a/src/Form/ConfigReadonlyExceptionsForm.php b/src/Form/ConfigReadonlyExceptionsForm.php
new file mode 100644
index 0000000..b207d68
--- /dev/null
+++ b/src/Form/ConfigReadonlyExceptionsForm.php
@@ -0,0 +1,296 @@
+<?php
+
+namespace Drupal\config_readonly\Form;
+
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Component\Serialization\Yaml;
+use Drupal\config_readonly\ConfigReadonlyExceptionsServiceProvider;
+
+/**
+ * Class ConfigReadonlyExceptionsForm.
+ *
+ * @package Drupal\config_readonly\Form
+ */
+class ConfigReadonlyExceptionsForm extends ConfigFormBase {
+
+  /**
+   * Drupal\Component\Serialization\Yaml definition.
+   *
+   * @var \Drupal\Component\Serialization\Yaml
+   */
+  protected $yaml;
+
+  /**
+   * Drupal\config_readonly\ConfigReadonlyExceptionsServiceProvider definition.
+   *
+   * @var \Drupal\config_readonly\ConfigReadonlyExceptionsServiceProvider
+   */
+  protected $exceptionsService;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    Yaml $serialization_yaml,
+    ConfigReadonlyExceptionsServiceProvider $exceptions_service
+  ) {
+    parent::__construct($config_factory);
+    $this->yaml = $serialization_yaml;
+    $this->exceptionsService = $exceptions_service;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('serialization.yaml'),
+      $container->get('config_readonly_exceptions')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'config_readonly.config_readonly_exceptions',
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'config_readonly_exceptions_form';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $config = $this->config('config_readonly.config_readonly_exceptions');
+
+    // Default values.
+    $exceptions = $config->get('exceptions');
+    $exceptions = ($exceptions ? $exceptions : []);
+
+    // State that the form needs to allow for a hierarchy (ie, multiple
+    // exceptions allowed).
+    $form['#tree'] = TRUE;
+
+    // How the user wants to manage their exceptions.
+    $exceptions_type = $config->get('exceptions_type');
+    $form['exceptions_type'] = [
+      '#type' => 'radios',
+      '#title' => $this->t('Exceptions management'),
+      '#default_value' => ($exceptions_type ? $exceptions_type : 'none'),
+      '#options' => [
+        'none' => $this->t('No exceptions are needed, all config should be read only.'),
+        'from_manual_config' => $this->t('Configure exceptions manually'),
+        'from_drush_config_ignore' => $this->t('Load exceptions automatically from a drush config-ignore path'),
+      ],
+      '#description' => $this->t('Decide whether you would like to control exceptions manually using this form or whether you would like to have them automatically loaded from your drush cimy/cexy config-ignore.yml file.'),
+    ];
+
+    // Initial number of names.
+    if (!$form_state->get('num_exceptions')) {
+      $form_state->set('num_exceptions', max(count($exceptions), 1));
+    }
+
+    // Container for our repeating fields.
+    $form['exceptions'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Manually set Exceptions'),
+      '#states' => [
+        'visible' => [
+          ':input[name="exceptions_type"]' => ['value' => 'from_manual_config'],
+        ],
+      ],
+      '#description' => $this->t('Write the filenames of your config yaml files (without the .yml extension). You may use "*" to add wildcards. For instance to exclude all contact forms from being read only you can exclude the form, field, and field storage settings with "contact.form.*", "field.field.contact_message.*", and "field.storage.contact_message.*".'),
+    ];
+
+    // Add our exceptions fields.
+    for ($x = 0; $x < $form_state->get('num_exceptions'); $x++) {
+      $form['exceptions'][$x]['exception'] = [
+        '#type' => 'textfield',
+        '#title' => $this->t('Exception @num', ['@num' => ($x + 1)]),
+        '#default_value' => (isset($exceptions[$x]) ? $exceptions[$x] : ''),
+      ];
+    }
+
+    // Container for our repeating fields.
+    $form['config_ignore'] = [
+      '#type' => 'fieldset',
+      '#title' => $this->t('Exceptions from Config Ignore file'),
+      '#states' => [
+        'visible' => [
+          ':input[name="exceptions_type"]' => ['value' => 'from_drush_config_ignore'],
+        ],
+      ],
+    ];
+
+    // Config ignore file path.
+    $config_ignore_path = $config->get('config_ignore_path');
+    $form['config_ignore']['config_ignore_path'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Config ignore path'),
+      '#default_value' => $config_ignore_path,
+      '#description' => $this->t('Please enter a path relative to your drupal root such as "../drush/config-ignore.yml".'),
+    ];
+
+    $config_ignore_exceptions = $this->exceptionsService->getExceptionsFromDrushConfigIgnore($config_ignore_path);
+    if ($config_ignore_exceptions) {
+      $form['config_ignore']['existing_title'] = [
+        '#markup' => '<strong>' . $this->t('Contents of the config ignore file') . '</strong>',
+      ];
+      if ($config_ignore_exceptions) {
+        $form['config_ignore']['existing'] = [
+          '#theme' => 'item_list',
+          '#type' => 'ul',
+          '#items' => $config_ignore_exceptions,
+        ];
+      }
+      else {
+        $form['config_ignore']['existing_warning'] = [
+          '#plain_text' => $this->t('If you have just changed the path, contents will be shown here after saving.'),
+        ];
+      }
+    }
+
+    $form = parent::buildForm($form, $form_state);
+
+    // Alternative action to add more exceptions.
+    $form['exceptions']['actions'] = [
+      '#type' => 'container',
+    ];
+    $form['exceptions']['actions']['add_exceptions'] = [
+      '#type' => 'submit',
+      '#id' => 'add_exceptions',
+      '#value' => $this->t('Add another exception'),
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+    if ($values['exceptions_type'] == 'from_drush_config_ignore') {
+      $this->exceptionsService->throwExceptions(FALSE);
+      $path = $values['config_ignore']['config_ignore_path'];
+      $patterns = $this->exceptionsService->getExceptionsFromDrushConfigIgnore($path);
+      $errors = $this->exceptionsService->getErrors();
+      if ($errors) {
+
+        // Set validation error as we had some sort of errors parsing the yml.
+        $error_string = implode(' ', $errors);
+        $form_state->setErrorByName('config_ignore_path', $error_string);
+      }
+    }
+    parent::validateForm($form, $form_state);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    $values = $form_state->getValues();
+
+    // Decide what action to take based on which button the user clicked.
+    switch ($form_state->getTriggeringElement()['#id']) {
+      case 'add_exceptions':
+        $this->addNewFields($form, $form_state);
+        break;
+
+      default:
+        $this->saveConfig($form, $form_state);
+    }
+  }
+
+  /**
+   * Handle adding new exception.
+   *
+   * @var array $Form
+   *   The form.
+   * @var FormStateInterface $form_state
+   *   The form state.
+   */
+  private function addNewFields(array &$form, FormStateInterface $form_state) {
+
+    // Add 1 to the number of exceptions.
+    $num_exceptions = $form_state->get('num_exceptions');
+    $form_state->set('num_exceptions', ($num_exceptions + 1));
+
+    // Rebuild the form on reload using the new number of exceptions.
+    $form_state->setRebuild();
+  }
+
+  /**
+   * Handle saving config.
+   *
+   * @var array $Form
+   *   The form.
+   * @var FormStateInterface $form_state
+   *   The form state.
+   */
+  private function saveConfig(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+    $values = $form_state->getValues();
+    $exceptions = $this->prepareExceptions($values['exceptions']);
+
+    // Save the config.
+    $exceptions_config = $this->config('config_readonly.config_readonly_exceptions');
+    $exceptions_config->set('exceptions_type', $values['exceptions_type']);
+    $exceptions_config->set('exceptions', $values['exceptions']);
+    $exceptions_config->set('config_ignore_path', $values['config_ignore']['config_ignore_path']);
+    $exceptions_config->save();
+  }
+
+  /**
+   * Prepare the exceptions for save.
+   *
+   * @param array $exceptions
+   *   The form state exceptions.
+   *
+   * @return array
+   *   The prepared exceptions ready for saving.
+   */
+  private function prepareExceptions($exceptions) {
+
+    // Remove the add more action from the exceptions.
+    unset($exceptions['actions']);
+
+    // Flatten the exceptions from a multidimensional array to a simple array.
+    array_walk($exceptions, array('self', 'flattenExceptions'));
+
+    // Trim all values to avoid for instance a single space remaining
+    // as a value.
+    $exceptions = array_map('trim', $exceptions);
+
+    // Remove empty values.
+    $exceptions = array_filter($exceptions);
+
+    // Reset array keys.
+    $exceptions = array_values($exceptions);
+
+    return $exceptions;
+  }
+
+  /**
+   * Callback to flatten the exceptions into a simple array.
+   *
+   * @param array $item
+   *   The exception array.
+   */
+  public static function flattenExceptions(&$item) {
+    $item = $item['exception'];
+  }
+
+}
diff --git a/src/ReadOnlyFormEvent.php b/src/ReadOnlyFormEvent.php
index df77b6c..d9e8aab 100644
--- a/src/ReadOnlyFormEvent.php
+++ b/src/ReadOnlyFormEvent.php
@@ -1,10 +1,5 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\config_readonly\ReadOnlyFormEvent.
- */
-
 namespace Drupal\config_readonly;
 
 use Drupal\Core\Form\FormStateInterface;
@@ -21,21 +16,39 @@ class ReadOnlyFormEvent extends Event {
 
   protected $readOnlyForm;
 
+  /**
+   * {@inheritdoc}
+   */
   public function __construct(FormStateInterface $form_state) {
-    $this->readOnlyForm = false;
+    $this->readOnlyForm = FALSE;
     $this->formState = $form_state;
   }
 
+  /**
+   * Get the form state.
+   *
+   * @return object
+   *   The form state.
+   */
   public function getFormState() {
     return $this->formState;
   }
 
+  /**
+   * Mark a form as read only.
+   */
   public function markFormReadOnly() {
-    $this->readOnlyForm = true;
+    $this->readOnlyForm = TRUE;
   }
 
+  /**
+   * Get the form read only status.
+   *
+   * @return bool
+   *   The form read only status.
+   */
   public function isFormReadOnly() {
     return $this->readOnlyForm;
   }
-}
 
+}
diff --git a/src/Tests/ReadOnlyConfigTest.php b/src/Tests/ReadOnlyConfigTest.php
index 7a23958..1789b6d 100644
--- a/src/Tests/ReadOnlyConfigTest.php
+++ b/src/Tests/ReadOnlyConfigTest.php
@@ -1,14 +1,8 @@
 <?php
 
-/**
- * @file
- * Contains \Drupal\config_readonly\Tests\ReadOnlyConfigTest.
- */
-
 namespace Drupal\config_readonly\Tests;
 
 use Drupal\simpletest\WebTestBase;
-use Drupal\Core\Site\Settings;
 
 /**
  * Tests read-only module config functionality.
@@ -19,12 +13,18 @@ class ReadOnlyConfigTest extends WebTestBase {
 
   public static $modules = ['config', 'config_readonly'];
 
+  /**
+   * Set up tests.
+   */
   public function setUp() {
     parent::setUp();
-    $this->adminUser = $this->createUser([], null, true);
+    $this->adminUser = $this->createUser([], NULL, TRUE);
     $this->drupalLogin($this->adminUser);
   }
 
+  /**
+   * Turn on read only.
+   */
   protected function turnOnReadOnlySetting() {
     $settings['settings']['config_readonly'] = (object) [
       'value' => TRUE,
@@ -33,6 +33,9 @@ class ReadOnlyConfigTest extends WebTestBase {
     $this->writeSettings($settings);
   }
 
+  /**
+   * Turn off read only.
+   */
   protected function turnOffReadOnlySetting() {
     $settings['settings']['config_readonly'] = (object) [
       'value' => FALSE,
@@ -41,6 +44,9 @@ class ReadOnlyConfigTest extends WebTestBase {
     $this->writeSettings($settings);
   }
 
+  /**
+   * Test that the status message is being shown appropriately.
+   */
   public function testModulePages() {
     $this->drupalGet('admin/modules');
     $this->assertNoText('This form will not be saved because the configuration active store is read-only.', 'Warning not shown on modules install page.');
@@ -65,6 +71,9 @@ class ReadOnlyConfigTest extends WebTestBase {
     $this->assert((string) $install_button['disabled'] == 'disabled', 'The install modules form button is disabled.');
   }
 
+  /**
+   * Test the site information page as a simple example.
+   */
   public function testSimpleConfig() {
     $this->drupalGet('admin/config/system/site-information');
     $this->assertNoText('This form will not be saved because the configuration active store is read-only.', 'Warning not shown on site information admin config page.');
@@ -74,6 +83,9 @@ class ReadOnlyConfigTest extends WebTestBase {
     $this->assertText('This form will not be saved because the configuration active store is read-only.', 'Warning shown on site information admin config page.');
   }
 
+  /**
+   * Test a simple import page as an example.
+   */
   public function testSingleImport() {
     $this->drupalGet('admin/config/development/configuration/single/import');
     $this->assertNoText('This form will not be saved because the configuration active store is read-only.', 'Warning not shown on single config import page.');
