diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc7213d3684ff7c36001d2505fa6714fd3b25de2..028c374785914214db0fead74539fbb9f08e4448 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,14 +6,14 @@ This is a changelog for Piwik platform developers. All changes for our HTTP API'
 
 ### New features
  * New segment `actionType` lets you segment all actions of a given type, eg. `actionType==events` or `actionType==downloads`. Action types values are: `pageviews`, `contents`, `sitesearches`, `events`, `outlinks`, `downloads`
-* The JavaScript Tracker method `PiwikTracker.setDomains()` can now handle paths. This means when setting eg `_paq.push(['setDomains, '*.piwik.org/website1'])` all link that goes to the same domain `piwik.org` but to any other path than `website1/*` will be treated as outlink.
+ * The JavaScript Tracker method `PiwikTracker.setDomains()` can now handle paths. This means when setting eg `_paq.push(['setDomains, '*.piwik.org/website1'])` all link that goes to the same domain `piwik.org` but to any other path than `website1/*` will be treated as outlink.
 
 ### Internal change
  * When generating a new plugin skeleton via `generate:plugin` command, plugin name must now contain only letters and numbers.
  * JavaScript Tracker tests no longer require `SQLite`. The existing MySQL configuration for tests is used now. In order to run the tests make sure Piwik is installed and `[database_tests]` is configured in `config/config.ini.php`.
  * The definitions for search engine and social network detection have been moved from bundled data files to a separate package (see [https://github.com/piwik/searchengine-and-social-list](https://github.com/piwik/searchengine-and-social-list)).
  * In [UI screenshot tests](https://developer.piwik.org/guides/tests-ui), a test environment `configOverride` setting should be no longer overwritten. Instead new values should be added to the existing `configOverride` array in PHP or JavaScript. For example instead of `testEnvironment.configOverride = {group: {name: 1}}` use `testEnvironment.overrideConfig('group', 'name', '1')`.
-
+ 
 ### New APIs
  * Add your own SMS/Text provider by creating a new class in the `SMSProvider` directory of your plugin. The class has to extend `Piwik\Plugins\MobileMessaging\SMSProvider` and implement the required methods.
  * Segments can now be composed by a union of multiple segments. To do this set an array of segments that shall be used for that segment `$segment->setUnionOfSegments(array('outlinkUrl', 'downloadUrl'))` instead of defining a SQL column.
@@ -21,6 +21,9 @@ This is a changelog for Piwik platform developers. All changes for our HTTP API'
 ### Deprecations
  * The method `DB::tableExists` was un-used and has been removed.
  
+### New commands
+ * New command `config:set` lets you set INI config options from the command line. This command can be used for convenience or for automation.
+
 ## Piwik 2.15.0 
 
 ### New commands
diff --git a/plugins/CoreAdminHome/Commands/SetConfig.php b/plugins/CoreAdminHome/Commands/SetConfig.php
new file mode 100644
index 0000000000000000000000000000000000000000..49b1925fd3fac90a42a8bc0060ae6a668b305f63
--- /dev/null
+++ b/plugins/CoreAdminHome/Commands/SetConfig.php
@@ -0,0 +1,97 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\Commands;
+
+use Piwik\Config;
+use Piwik\Plugin\ConsoleCommand;
+use Piwik\Plugins\CoreAdminHome\Commands\SetConfig\ConfigSettingManipulation;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SetConfig extends ConsoleCommand
+{
+    protected function configure()
+    {
+        $this->setName('config:set');
+        $this->setDescription('Set one or more config settings in the file config/config.ini.php');
+        $this->addArgument('assignment', InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+            "List of config setting assignments, eg, Section.key=1 or Section.array_key[]=value");
+        $this->addOption('section', null, InputOption::VALUE_REQUIRED, 'The section the INI config setting belongs to.');
+        $this->addOption('key', null, InputOption::VALUE_REQUIRED, 'The name of the INI config setting.');
+        $this->addOption('value', null, InputOption::VALUE_REQUIRED, 'The value of the setting. (Not JSON encoded)');
+        $this->setHelp("This command can be used to set INI config settings on a Piwik instance.
+
+You can set config values two ways, via --section, --key, --value or by command arguments.
+
+To use --section, --key, --value, simply supply those options. You can only set one setting this way, and you cannot
+append to arrays.
+
+To use arguments, supply one or more arguments in the following format: Section.config_setting_name=\"value\"
+'Section' is the name of the section, 'config_setting_name' the name of the setting and 'value' is the value.
+NOTE: 'value' must be JSON encoded, so Section.config_setting_name=\"value\" would work but
+Section.config_setting_name=value would not.
+
+To append to an array setting, supply an argument like this: Section.config_setting_name[]=\"value to append\"
+
+To reset an array setting, supply an argument like this: Section.config_setting_name=[]
+Resetting an array will not work if the array has default values in global.ini.php (such as, [log] log_writers).
+In this case the values in global.ini.php will be used, since there is no way to explicitly set an
+array setting to empty in INI config.
+
+Use the --piwik-domain option to specify which instance to modify.
+
+");
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output)
+    {
+        $section = $input->getOption('section');
+        $key = $input->getOption('key');
+        $value = $input->getOption('value');
+
+        $manipulations = $this->getAssignments($input);
+
+        $isSingleAssignment = !empty($section) && !empty($key) && $value !== false;
+        if ($isSingleAssignment) {
+            $manipulations[] = new ConfigSettingManipulation($section, $key, $value);
+        }
+
+        if (empty($manipulations)) {
+            throw new \InvalidArgumentException("Nothing to assign. Add assignments as arguments or use the "
+                . "--section, --key and --value options.");
+        }
+
+        $config = Config::getInstance();
+        foreach ($manipulations as $manipulation) {
+            $manipulation->manipulate($config);
+
+            $output->writeln("<info>Setting [{$manipulation->getSectionName()}] {$manipulation->getName()} = {$manipulation->getValueString()}</info>");
+        }
+
+        $config->forceSave();
+
+        $this->writeSuccessMessage($output, array("Done."));
+    }
+
+    /**
+     * @return ConfigSettingManipulation[]
+     */
+    private function getAssignments(InputInterface $input)
+    {
+        $assignments = $input->getArgument('assignment');
+
+        $result = array();
+        foreach ($assignments as $assignment) {
+            $result[] = ConfigSettingManipulation::make($assignment);
+        }
+        return $result;
+    }
+}
\ No newline at end of file
diff --git a/plugins/CoreAdminHome/Commands/SetConfig/ConfigSettingManipulation.php b/plugins/CoreAdminHome/Commands/SetConfig/ConfigSettingManipulation.php
new file mode 100644
index 0000000000000000000000000000000000000000..e13fa3baa69ebb7483ba06f300603005c3f0d9ee
--- /dev/null
+++ b/plugins/CoreAdminHome/Commands/SetConfig/ConfigSettingManipulation.php
@@ -0,0 +1,176 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\Commands\SetConfig;
+
+use Piwik\Config;
+
+/**
+ * Representation of a INI config manipulation operation. Only supports two types
+ * of manipulations: appending to a config array and assigning a config value.
+ */
+class ConfigSettingManipulation
+{
+    /**
+     * @var string
+     */
+    private $sectionName;
+
+    /**
+     * @var string
+     */
+    private $name;
+
+    /**
+     * @var string
+     */
+    private $value;
+
+    /**
+     * @var bool
+     */
+    private $isArrayAppend;
+
+    /**
+     * @param string $sectionName
+     * @param string $name
+     * @param string $value
+     * @param bool $isArrayAppend
+     */
+    public function __construct($sectionName, $name, $value, $isArrayAppend = false)
+    {
+        $this->sectionName = $sectionName;
+        $this->name = $name;
+        $this->value = $value;
+        $this->isArrayAppend = $isArrayAppend;
+    }
+
+    /**
+     * Performs the INI config manipulation.
+     *
+     * @param Config $config
+     * @throws \Exception if trying to append to a non-array setting value or if trying to set an
+     *                    array value to a non-array setting
+     */
+    public function manipulate(Config $config)
+    {
+        if ($this->isArrayAppend) {
+            $this->appendToArraySetting($config);
+        } else {
+            $this->setSingleConfigValue($config);
+        }
+    }
+
+    private function setSingleConfigValue(Config $config)
+    {
+        $sectionName = $this->sectionName;
+        $section = $config->$sectionName;
+
+        if (isset($section[$this->name])
+            && is_array($section[$this->name])
+            && !is_array($this->value)
+        ) {
+            throw new \Exception("Trying to set non-array value to array setting " . $this->getSettingString() . ".");
+        }
+
+        $section[$this->name] = $this->value;
+        $config->$sectionName = $section;
+    }
+
+    private function appendToArraySetting(Config $config)
+    {
+        $sectionName = $this->sectionName;
+        $section = $config->$sectionName;
+
+        if (isset($section[$this->name])
+            && !is_array($section[$this->name])
+        ) {
+            throw new \Exception("Trying to append to non-array setting value " . $this->getSettingString() . ".");
+        }
+
+        $section[$this->name][] = $this->value;
+        $config->$sectionName = $section;
+    }
+
+    /**
+     * Creates a ConfigSettingManipulation instance from a string like:
+     *
+     * `SectionName.setting_name=value`
+     *
+     * or
+     *
+     * `SectionName.setting_name[]=value`
+     *
+     * The value must be JSON so `="string"` will work but `=string` will not.
+     *
+     * @param string $assignment
+     * @return self
+     */
+    public static function make($assignment)
+    {
+        if (!preg_match('/^([a-zA-Z0-9_]+)\.([a-zA-Z0-9_]+)(\[\])?=(.*)/', $assignment, $matches)) {
+            throw new \InvalidArgumentException("Invalid assignment string '$assignment': expected section.name=value or section.name[]=value");
+        }
+
+        $section = $matches[1];
+        $name = $matches[2];
+        $isAppend = !empty($matches[3]);
+
+        $value = json_decode($matches[4], $isAssoc = true);
+        if ($value === null) {
+            throw new \InvalidArgumentException("Invalid assignment string '$assignment': could not parse value as JSON");
+        }
+
+        return new self($section, $name, $value, $isAppend);
+    }
+
+    private function getSettingString()
+    {
+        return "[{$this->sectionName}] {$this->name}";
+    }
+
+    /**
+     * @return string
+     */
+    public function getSectionName()
+    {
+        return $this->sectionName;
+    }
+
+    /**
+     * @return string
+     */
+    public function getName()
+    {
+        return $this->name;
+    }
+
+    /**
+     * @return string
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * @return boolean
+     */
+    public function isArrayAppend()
+    {
+        return $this->isArrayAppend;
+    }
+
+    /**
+     * @return string
+     */
+    public function getValueString()
+    {
+        return json_encode($this->value);
+    }
+}
\ No newline at end of file
diff --git a/plugins/CoreAdminHome/tests/Integration/SetConfigTest.php b/plugins/CoreAdminHome/tests/Integration/SetConfigTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..bd1ed5e6181daf88f6b360a8f0e8a8451dbc002f
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Integration/SetConfigTest.php
@@ -0,0 +1,194 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\tests\Integration\Commands;
+
+use Interop\Container\ContainerInterface;
+use Piwik\Application\Kernel\GlobalSettingsProvider;
+use Piwik\Config;
+use Piwik\Tests\Framework\TestCase\ConsoleCommandTestCase;
+use Piwik\Url;
+
+/**
+ * @group CoreAdminHome
+ * @group CoreAdminHome_Integration
+ */
+class SetConfigTest extends ConsoleCommandTestCase
+{
+    const TEST_CONFIG_PATH = '/tmp/test.config.ini.php';
+
+    public static function setUpBeforeClass()
+    {
+        self::removeTestConfigFile();
+
+        parent::setUpBeforeClass();
+    }
+
+    public function setUp()
+    {
+        self::removeTestConfigFile();
+
+        parent::setUp();
+    }
+
+    public function test_Command_SucceedsWhenOptionsUsed()
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'config:set',
+            '--section' => 'MySection',
+            '--key' => 'setting',
+            '--value' => 'myvalue',
+            '-vvv' => false,
+        ));
+
+        $this->assertEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+
+        $config = $this->makeNewConfig();
+        $this->assertEquals(array('setting' => 'myvalue'), $config->MySection);
+
+        $this->assertContains('Setting [MySection] setting = "myvalue"', $this->applicationTester->getDisplay());
+    }
+
+    /**
+     * @dataProvider getInvalidArgumentsForTest
+     */
+    public function test_Command_FailsWhenInvalidArgumentsUsed($invalidArgument)
+    {
+        $code = $this->applicationTester->run(array(
+            'command' => 'config:set',
+            'assignment' => array($invalidArgument),
+            '-vvv' => false,
+        ));
+
+        $this->assertNotEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+        $this->assertContains('Invalid assignment string', $this->applicationTester->getDisplay());
+    }
+
+    public function getInvalidArgumentsForTest()
+    {
+        return array(
+            array("garbage"),
+            array("ab&cd.ghi=23"),
+            array("section.value = 34"),
+            array("section.value = notjson"),
+            array("section.array[0]=23"),
+        );
+    }
+
+    public function test_Command_SucceedsWhenArgumentsUsed()
+    {
+        $config = Config::getInstance();
+        $config->General['trusted_hosts'] = array('www.trustedhost.com');
+        $config->MySection['other_array_value'] = array('1', '2');
+        $config->forceSave();
+
+        $code = $this->applicationTester->run(array(
+            'command' => 'config:set',
+            'assignment' => array(
+                'General.action_url_category_delimiter="+"',
+                'General.trusted_hosts[]="www.trustedhost2.com"',
+                'MySection.array_value=["abc","def"]',
+                'MySection.object_value={"abc":"def"}',
+                'MySection.other_array_value=[]',
+            ),
+            '-vvv' => false,
+        ));
+
+        $this->assertEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+
+        $config = self::makeNewConfig(); // create a new config instance so we read what's in the file
+
+        $this->assertEquals('+', $config->General['action_url_category_delimiter']);
+        $this->assertEquals(array('www.trustedhost.com', 'www.trustedhost2.com'), $config->General['trusted_hosts']);
+        $this->assertEquals(array('abc', 'def'), $config->MySection['array_value']);
+        $this->assertEquals(array('def'), $config->MySection['object_value']);
+        $this->assertArrayNotHasKey('other_array_value', $config->MySection);
+
+        $this->assertContains("Done.", $this->applicationTester->getDisplay());
+    }
+
+    /**
+     * @dataProvider getOptionsForSettingValueToZeroTests
+     */
+    public function test_Command_SucceedsWhenSettingValueToZero($options)
+    {
+        $config = Config::getInstance();
+        $config->Tracker['debug'] = 1;
+        $config->forceSave();
+
+        $code = $this->applicationTester->run($options);
+
+        $this->assertEquals(0, $code, $this->getCommandDisplayOutputErrorMessage());
+
+        $config = self::makeNewConfig();
+
+        $this->assertEquals(0, $config->Tracker['debug']);
+        $this->assertContains("Done.", $this->applicationTester->getDisplay());
+    }
+
+    public function getOptionsForSettingValueToZeroTests()
+    {
+        return array(
+            array(
+                array(
+                    'command' => 'config:set',
+                    '--section' => 'Tracker',
+                    '--key' => 'debug',
+                    '--value' => 0,
+                ),
+            ),
+            array(
+                array(
+                    'command' => 'config:set',
+                    'assignment' => array(
+                        'Tracker.debug=0',
+                    ),
+                ),
+            ),
+        );
+    }
+
+    private static function getTestConfigFilePath()
+    {
+        return PIWIK_INCLUDE_PATH . self::TEST_CONFIG_PATH;
+    }
+
+    public static function provideContainerConfigBeforeClass()
+    {
+        return array(
+            // use a config instance that will save to a test INI file
+            'Piwik\Config' => function (ContainerInterface $c) {
+                /** @var GlobalSettingsProvider $actualGlobalSettingsProvider */
+                $actualGlobalSettingsProvider = $c->get('Piwik\Application\Kernel\GlobalSettingsProvider');
+
+                $config = SetConfigTest::makeNewConfig();
+
+                // copy over sections required for tests
+                $config->tests = $actualGlobalSettingsProvider->getSection('tests');
+                $config->database = $actualGlobalSettingsProvider->getSection('database');
+                $config->database_tests = $actualGlobalSettingsProvider->getSection('database_tests');
+
+                return $config;
+            },
+        );
+    }
+
+    private static function makeNewConfig()
+    {
+        $settings = new GlobalSettingsProvider(null, SetConfigTest::getTestConfigFilePath());
+        return new Config($settings);
+    }
+
+    private static function removeTestConfigFile()
+    {
+        $configPath = self::getTestConfigFilePath();
+        if (file_exists($configPath)) {
+            unlink($configPath);
+        }
+    }
+}
diff --git a/plugins/CoreAdminHome/tests/Unit/SetConfig/ConfigSettingManipulationTest.php b/plugins/CoreAdminHome/tests/Unit/SetConfig/ConfigSettingManipulationTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..88a5d0c79c72db2fe04db852e49b25a39feb6f67
--- /dev/null
+++ b/plugins/CoreAdminHome/tests/Unit/SetConfig/ConfigSettingManipulationTest.php
@@ -0,0 +1,165 @@
+<?php
+/**
+ * Piwik - free/libre analytics platform
+ *
+ * @link http://piwik.org
+ * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
+ */
+
+namespace Piwik\Plugins\CoreAdminHome\tests\Unit\Commands\SetConfig;
+
+use Piwik\Config;
+use Piwik\Plugins\CoreAdminHome\Commands\SetConfig\ConfigSettingManipulation;
+
+// phpunit mocks can't return references, so we need a manual one
+class DumbMockConfig extends \Piwik\Config
+{
+    /**
+     * @var array
+     */
+    public $mockConfigData;
+
+    public function __construct()
+    {
+        // empty
+    }
+
+    public function &__get($sectionName)
+    {
+        if (!isset($this->mockConfigData[$sectionName])) {
+            $this->mockConfigData[$sectionName] = array();
+        }
+
+        $result =& $this->mockConfigData[$sectionName];
+        return $result;
+    }
+
+    public function __set($sectionName, $section)
+    {
+        $this->mockConfigData[$sectionName] = $section;
+    }
+}
+
+/**
+ * @group CoreAdminHome
+ * @group CoreAdminHome_Unit
+ */
+class ConfigSettingManipulationTest extends \PHPUnit_Framework_TestCase
+{
+    /**
+     * @var Config
+     */
+    private $mockConfig;
+
+    protected function setUp()
+    {
+        $this->mockConfig = new DumbMockConfig();
+        $this->mockConfigData = array();
+    }
+
+    /**
+     * @dataProvider getTestDataForMake
+     */
+    public function test_make_CreatesCorrectManipulation($assignmentString, $expectedSectionName, $expectedSettingName,
+                                                         $expectedSettingValue, $expectedIsArrayAppend)
+    {
+        $manipulation = ConfigSettingManipulation::make($assignmentString);
+
+        $this->assertEquals($expectedSectionName, $manipulation->getSectionName());
+        $this->assertEquals($expectedSettingName, $manipulation->getName());
+        $this->assertEquals($expectedSettingValue, $manipulation->getValue());
+        $this->assertEquals($expectedIsArrayAppend, $manipulation->isArrayAppend());
+    }
+
+    public function getTestDataForMake()
+    {
+        return array(
+            // normal assign
+            array("General.myconfig=0", "General", "myconfig", 0, false),
+
+            // array append
+            array("General.myconfig444[]=5", "General", "myconfig444", 5, true),
+
+            // assign array
+            array("1General1.2config2=[\"abc\",\"def\"]", "1General1", "2config2", array('abc', 'def'), false),
+
+            // assign string
+            array("MySection.value=\"ghi\"", "MySection", "value", "ghi", false),
+
+            // assign boolean
+            array("MySection.value=false", "MySection", "value", false, false),
+            array("MySection.value=true", "MySection", "value", true, false),
+        );
+    }
+
+    /**
+     * @dataProvider getFailureTestDataForMake
+     * @expectedException \InvalidArgumentException
+     * @expectedExceptionMessage Invalid assignment string
+     */
+    public function test_make_ThrowsWhenInvalidAssignmentStringSupplied($assignmentString)
+    {
+        ConfigSettingManipulation::make($assignmentString);
+    }
+
+    public function getFailureTestDataForMake()
+    {
+        return array(
+            array("General&.value=1"),
+            array("General.val&*ue=12"),
+            array("General.value=[notjson]"),
+            array("General.value=notjson"),
+            array("General.array[abc]=\"def\""),
+        );
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage Trying to append to non-array setting value
+     */
+    public function test_manipulate_ThrowsIfAppendingNonArraySetting()
+    {
+        $this->mockConfig->mockConfigData['General']['config'] = "5";
+
+        $manipulation = new ConfigSettingManipulation("General", "config", "10", true);
+        $manipulation->manipulate($this->mockConfig);
+    }
+
+    /**
+     * @expectedException \Exception
+     * @expectedExceptionMessage Trying to set non-array value to array setting
+     */
+    public function test_manipulate_ThrowsIfAssigningNonArrayValue_ToArraySetting()
+    {
+        $this->mockConfig->mockConfigData['General']['config'] = array("5");
+
+        $manipulation = new ConfigSettingManipulation("General", "config", "10", false);
+        $manipulation->manipulate($this->mockConfig);
+    }
+
+    /**
+     * @dataProvider getTestDataForManipulate
+     */
+    public function test_manipulate_CorrectlyManipulatesConfig($sectionName, $name, $value, $isArrayAppend, $expectedConfig)
+    {
+        $manipulation = new ConfigSettingManipulation($sectionName, $name, $value, $isArrayAppend);
+        $manipulation->manipulate($this->mockConfig);
+
+        $this->assertEquals($expectedConfig, $this->mockConfig->mockConfigData);
+    }
+
+    public function getTestDataForManipulate()
+    {
+        return array(
+            // normal assign (string, int, array, bool)
+            array("Section", "config_setting", "stringvalue", false, array("Section" => array("config_setting" => "stringvalue"))),
+            array("Section", "config_setting", 25, false, array("Section" => array("config_setting" => 25))),
+            array("Section", "config_setting", array('a' => 'b'), false, array("Section" => array("config_setting" => array('a' => 'b')))),
+            array("Section", "config_setting", false, false, array("Section" => array("config_setting" => false))),
+
+            // array append
+            array("Section", "config_setting", "value", true, array("Section" => array("config_setting" => array('value')))),
+            array("Section", "config_setting", array(1,2), true, array("Section" => array("config_setting" => array(array(1,2))))),
+        );
+    }
+}
\ No newline at end of file