How can I force changes to go to settings.local.php? I have a settings.php file in git which has the logic to include settings.local.php already uncommented, with settings.local.php in .gitignore for security purposes.

I would like to be able to install Drupal (8.3.3) so that any changes which are made by the installer (by default: hash salt, databases array, install profile, sync directory location) are made to settings.local.php rather than settings.php as the installation currently does. Is this possible?
I think it's possible, but you'll need a custom installation profile to do it. 

drupal_rewrite_settings() takes a 2nd parameter for a custom path to settings.php, but none of the default core methods make use of it, they let it fallback to sites/default/settings.php. It's used in three key places:


Drupal\Core\Installer\Form\SiteSettingsForm::submitForm()
drupal_install_config_directories()
install_write_profile() (deprecated)


The form and install_write_profile() are invoked from an install task, so those can be altered with hook_install_tasks_alter(). drupal_install_config_directories() is invoked from the form submit method, which you'll be overriding anyway, so you can replace it with your own method. 

A completely untested example:

profiles/foo/foo.profile

function foo_install_tasks_alter(&amp;$tasks, $install_state) {
    $tasks['install_write_profile']['function'] = 'foo_install_write_profile';
    $tasks['install_settings_form']['function'] = 'Drupal\foo\Form\SiteSettingsForm';
}

function foo_install_write_profile($install_state) {
    // Only need to write to settings.php if it is possible. The primary storage
    // for the install profile is the core.extension configuration.
    $settings_path = \Drupal::service('site.path') . '/local.settings.php';

    // Make sure the file exists.
    touch($settings_path);

    if (is_writable($settings_path)) {
        // Remember the profile which was used.
        $settings['settings']['install_profile'] = (object) [
            'value' =&gt; $install_state['parameters']['profile'],
            'required' =&gt; TRUE,
        ];
        drupal_rewrite_settings($settings, $settings_path);
    }
    elseif (($settings_profile = Settings::get('install_profile')) &amp;&amp; $settings_profile !== $install_state['parameters']['profile']) {
        throw new InstallProfileMismatchException($install_state['parameters']['profile'], $settings_profile, $settings_path, \Drupal::translation());
    }
}


profiles/foo/src/Form/SiteSettingsForm.php

namespace Drupal\foo\Form;

use Drupal\Core\Installer\Form\SiteSettingsForm as BaseForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Component\Utility\Crypt;

class SiteSettingsForm extends BaseForm {

    public function submitForm(array &amp;$form, FormStateInterface $form_state) {
        global $install_state;

        // Update global settings array and save.
        $settings = [];
        $database = $form_state-&gt;get('database');
        $settings['databases']['default']['default'] = (object) [
            'value'    =&gt; $database,
            'required' =&gt; TRUE,
        ];
        $settings['settings']['hash_salt'] = (object) [
            'value'    =&gt; Crypt::randomBytesBase64(55),
            'required' =&gt; TRUE,
        ];
        // Remember the profile which was used.
        $settings['settings']['install_profile'] = (object) [
            'value' =&gt; $install_state['parameters']['profile'],
            'required' =&gt; TRUE,
        ];

        $settings_path = \Drupal::service('site.path') . '/local.settings.php';
        drupal_rewrite_settings($settings, $settings_path);

        // Add the config directories to settings.php.
        $this-&gt;installConfigDirectories();

        // Indicate that the settings file has been verified, and check the database
        // for the last completed task, now that we have a valid connection. This
        // last step is important since we want to trigger an error if the new
        // database already has Drupal installed.
        $install_state['settings_verified'] = TRUE;
        $install_state['config_verified'] = TRUE;
        $install_state['database_verified'] = TRUE;
        $install_state['completed_task'] = install_verify_completed_task();
    }

    protected function installConfigDirectories() {
        global $config_directories;

        // Add a randomized config directory name to settings.php, unless it was
        // manually defined in the existing already.
        if (empty($config_directories[CONFIG_SYNC_DIRECTORY])) {
            $config_directories[CONFIG_SYNC_DIRECTORY] = \Drupal::service('site.path') . '/files/config_' . Crypt::randomBytesBase64(55) . '/sync';
            $settings['config_directories'][CONFIG_SYNC_DIRECTORY] = (object) [
                'value' =&gt; $config_directories[CONFIG_SYNC_DIRECTORY],
                'required' =&gt; TRUE,
            ];
            // Rewrite settings.php, which also sets the value as global variable.
            $settings_path = \Drupal::service('site.path') . '/local.settings.php';
            drupal_rewrite_settings($settings, $settings_path);
        }

        // This should never fail, since if the config directory was specified in
        // settings.php it will have already been created and verified earlier, and
        // if it wasn't specified in settings.php, it is created here inside the
        // public files directory, which has already been verified to be writable
        // itself. But if it somehow fails anyway, the installation cannot proceed.
        // Bail out using a similar error message as in system_requirements().
        if (!file_prepare_directory($config_directories[CONFIG_SYNC_DIRECTORY], FILE_CREATE_DIRECTORY)
            &amp;&amp; !file_exists($config_directories[CONFIG_SYNC_DIRECTORY])) {
            throw new Exception(t('The directory %directory could not be created. To proceed with the installation, either create the directory or ensure that the installer has the permissions to create it automatically. For more information, see the &lt;a href=":handbook_url"&gt;online handbook&lt;/a&gt;.', [
                '%directory' =&gt; config_get_config_directory(CONFIG_SYNC_DIRECTORY),
                ':handbook_url' =&gt; 'https://www.drupal.org/server-permissions',
            ]));
        }
        elseif (is_writable($config_directories[CONFIG_SYNC_DIRECTORY])) {
            // Put a README.txt into the sync config directory. This is required so that
            // they can later be added to git. Since this directory is auto-created, we
            // have to write out the README rather than just adding it to the drupal core
            // repo.
            $text = 'This directory contains configuration to be imported into your Drupal site. To make this configuration active, visit admin/config/development/configuration/sync.' . ' For information about deploying configuration between servers, see https://www.drupal.org/documentation/administer/config';
            file_put_contents(config_get_config_directory(CONFIG_SYNC_DIRECTORY) . '/README.txt', $text);
        }
    }

}


The last step, if you want this to be fully automated and possibly avoid errors during installation, is an awkward one. You need to programatically uncomment the include for local.settings.php at the bottom of settings.php. 

I'm not aware of any Drupal helpers for that (might be wrong though), so you can probably approach that as a general PHP/other task. 

I think the top of the foo_install_write_profile() function would be as good a place as any to do it, unless of course your dev workflow means you can do it before you even carry out the installation (probably best if possible).