diff --git a/build.gradle b/build.gradle
index e498101a16a..4484d3fc14c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -231,6 +231,7 @@ subprojects {
     "org.elasticsearch.gradle:build-tools:${version}": ':build-tools',
     "org.elasticsearch:rest-api-spec:${version}": ':rest-api-spec',
     "org.elasticsearch:elasticsearch:${version}": ':core',
+    "org.elasticsearch:elasticsearch-cli:${version}": ':core:cli',
     "org.elasticsearch.client:elasticsearch-rest-client:${version}": ':client:rest',
     "org.elasticsearch.client:elasticsearch-rest-client-sniffer:${version}": ':client:sniffer',
     "org.elasticsearch.client:elasticsearch-rest-high-level-client:${version}": ':client:rest-high-level',
diff --git a/core/build.gradle b/core/build.gradle
index fe60cd8b1cf..b5ad8eb5c32 100644
--- a/core/build.gradle
+++ b/core/build.gradle
@@ -58,7 +58,7 @@ dependencies {
   compile 'org.elasticsearch:securesm:1.1'
 
   // utilities
-  compile 'net.sf.jopt-simple:jopt-simple:5.0.2'
+  compile "org.elasticsearch:elasticsearch-cli:${version}"
   compile 'com.carrotsearch:hppc:0.7.1'
 
   // time handling, remove with java 8 time
@@ -265,6 +265,12 @@ if (JavaVersion.current() > JavaVersion.VERSION_1_8) {
 dependencyLicenses {
   mapping from: /lucene-.*/, to: 'lucene'
   mapping from: /jackson-.*/, to: 'jackson'
+  dependencies = project.configurations.runtime.fileCollection {
+    it.group.startsWith('org.elasticsearch') == false ||
+            // keep the following org.elasticsearch jars in
+            (it.name == 'jna' ||
+             it.name == 'securesm')
+  }
 }
 
 if (isEclipse == false || project.path == ":core-tests") {
diff --git a/core/cli/build.gradle b/core/cli/build.gradle
new file mode 100644
index 00000000000..fc93523f6b7
--- /dev/null
+++ b/core/cli/build.gradle
@@ -0,0 +1,36 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import org.elasticsearch.gradle.precommit.PrecommitTasks
+
+apply plugin: 'elasticsearch.build'
+
+archivesBaseName = 'elasticsearch-cli'
+
+dependencies {
+    compile 'net.sf.jopt-simple:jopt-simple:5.0.2'
+}
+
+test.enabled = false
+// Since CLI does not depend on :core, it cannot run the jarHell task
+jarHell.enabled = false
+
+forbiddenApisMain {
+    signaturesURLs = [PrecommitTasks.getResource('/forbidden/jdk-signatures.txt')]
+}
diff --git a/core/licenses/jopt-simple-5.0.2.jar.sha1 b/core/cli/licenses/jopt-simple-5.0.2.jar.sha1
similarity index 100%
rename from core/licenses/jopt-simple-5.0.2.jar.sha1
rename to core/cli/licenses/jopt-simple-5.0.2.jar.sha1
diff --git a/core/licenses/jopt-simple-LICENSE.txt b/core/cli/licenses/jopt-simple-LICENSE.txt
similarity index 100%
rename from core/licenses/jopt-simple-LICENSE.txt
rename to core/cli/licenses/jopt-simple-LICENSE.txt
diff --git a/core/licenses/jopt-simple-NOTICE.txt b/core/cli/licenses/jopt-simple-NOTICE.txt
similarity index 100%
rename from core/licenses/jopt-simple-NOTICE.txt
rename to core/cli/licenses/jopt-simple-NOTICE.txt
diff --git a/core/src/main/java/org/elasticsearch/cli/Command.java b/core/cli/src/main/java/org/elasticsearch/cli/Command.java
similarity index 81%
rename from core/src/main/java/org/elasticsearch/cli/Command.java
rename to core/cli/src/main/java/org/elasticsearch/cli/Command.java
index a60dece2611..78a9f31283d 100644
--- a/core/src/main/java/org/elasticsearch/cli/Command.java
+++ b/core/cli/src/main/java/org/elasticsearch/cli/Command.java
@@ -23,11 +23,6 @@ import joptsimple.OptionException;
 import joptsimple.OptionParser;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
-import org.apache.logging.log4j.Level;
-import org.apache.lucene.util.SetOnce;
-import org.elasticsearch.common.SuppressForbidden;
-import org.elasticsearch.common.logging.LogConfigurator;
-import org.elasticsearch.common.settings.Settings;
 
 import java.io.Closeable;
 import java.io.IOException;
@@ -55,12 +50,13 @@ public abstract class Command implements Closeable {
         this.description = description;
     }
 
-    final SetOnce<Thread> shutdownHookThread = new SetOnce<>();
+    private Thread shutdownHookThread;
 
     /** Parses options for this command from args and executes it. */
     public final int main(String[] args, Terminal terminal) throws Exception {
         if (addShutdownHook()) {
-            shutdownHookThread.set(new Thread(() -> {
+
+            shutdownHookThread = new Thread(() -> {
                 try {
                     this.close();
                 } catch (final IOException e) {
@@ -75,16 +71,11 @@ public abstract class Command implements Closeable {
                         throw new AssertionError(impossible);
                     }
                 }
-            }));
-            Runtime.getRuntime().addShutdownHook(shutdownHookThread.get());
+            });
+            Runtime.getRuntime().addShutdownHook(shutdownHookThread);
         }
 
-        if (shouldConfigureLoggingWithoutConfig()) {
-            // initialize default for es.logger.level because we will not read the log4j2.properties
-            final String loggerLevel = System.getProperty("es.logger.level", Level.INFO.name());
-            final Settings settings = Settings.builder().put("logger.level", loggerLevel).build();
-            LogConfigurator.configureWithoutConfig(settings);
-        }
+        beforeExecute();
 
         try {
             mainWithoutErrorHandling(args, terminal);
@@ -103,14 +94,10 @@ public abstract class Command implements Closeable {
     }
 
     /**
-     * Indicate whether or not logging should be configured without reading a log4j2.properties. Most commands should do this because we do
-     * not configure logging for CLI tools. Only commands that configure logging on their own should not do this.
-     *
-     * @return true if logging should be configured without reading a log4j2.properties file
+     * Setup method to be executed before parsing or execution of the command being run. Any exceptions thrown by the
+     * method will not be cleanly caught by the parser.
      */
-    protected boolean shouldConfigureLoggingWithoutConfig() {
-        return true;
-    }
+    protected void beforeExecute() {}
 
     /**
      * Executes the command, but all errors are thrown.
@@ -166,6 +153,11 @@ public abstract class Command implements Closeable {
         return true;
     }
 
+    /** Gets the shutdown hook thread if it exists **/
+    Thread getShutdownHookThread() {
+        return shutdownHookThread;
+    }
+
     @Override
     public void close() throws IOException {
 
diff --git a/core/src/main/java/org/elasticsearch/cli/ExitCodes.java b/core/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java
similarity index 100%
rename from core/src/main/java/org/elasticsearch/cli/ExitCodes.java
rename to core/cli/src/main/java/org/elasticsearch/cli/ExitCodes.java
diff --git a/core/src/main/java/org/elasticsearch/cli/MultiCommand.java b/core/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java
similarity index 100%
rename from core/src/main/java/org/elasticsearch/cli/MultiCommand.java
rename to core/cli/src/main/java/org/elasticsearch/cli/MultiCommand.java
diff --git a/core/cli/src/main/java/org/elasticsearch/cli/SuppressForbidden.java b/core/cli/src/main/java/org/elasticsearch/cli/SuppressForbidden.java
new file mode 100644
index 00000000000..882414a0eaa
--- /dev/null
+++ b/core/cli/src/main/java/org/elasticsearch/cli/SuppressForbidden.java
@@ -0,0 +1,34 @@
+/*
+ * Licensed to Elasticsearch under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch licenses this file to you under
+ * the Apache License, Version 2.0 (the "License"); you may
+ * not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.elasticsearch.cli;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation to suppress forbidden-apis errors inside a whole class, a method, or a field.
+ */
+@Retention(RetentionPolicy.CLASS)
+@Target({ ElementType.CONSTRUCTOR, ElementType.FIELD, ElementType.METHOD, ElementType.TYPE })
+public @interface SuppressForbidden {
+    String reason();
+}
+
diff --git a/core/src/main/java/org/elasticsearch/cli/Terminal.java b/core/cli/src/main/java/org/elasticsearch/cli/Terminal.java
similarity index 99%
rename from core/src/main/java/org/elasticsearch/cli/Terminal.java
rename to core/cli/src/main/java/org/elasticsearch/cli/Terminal.java
index d42e3475dc4..85abd616774 100644
--- a/core/src/main/java/org/elasticsearch/cli/Terminal.java
+++ b/core/cli/src/main/java/org/elasticsearch/cli/Terminal.java
@@ -19,8 +19,6 @@
 
 package org.elasticsearch.cli;
 
-import org.elasticsearch.common.SuppressForbidden;
-
 import java.io.BufferedReader;
 import java.io.Console;
 import java.io.IOException;
diff --git a/core/src/main/java/org/elasticsearch/cli/UserException.java b/core/cli/src/main/java/org/elasticsearch/cli/UserException.java
similarity index 100%
rename from core/src/main/java/org/elasticsearch/cli/UserException.java
rename to core/cli/src/main/java/org/elasticsearch/cli/UserException.java
diff --git a/core/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java b/core/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java
index d9d19a56a2f..44fcb37d083 100644
--- a/core/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java
+++ b/core/src/main/java/org/elasticsearch/cli/EnvironmentAwareCommand.java
@@ -22,7 +22,9 @@ package org.elasticsearch.cli;
 import joptsimple.OptionSet;
 import joptsimple.OptionSpec;
 import joptsimple.util.KeyValuePair;
+import org.apache.logging.log4j.Level;
 import org.elasticsearch.common.SuppressForbidden;
+import org.elasticsearch.common.logging.LogConfigurator;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.env.Environment;
 import org.elasticsearch.node.InternalSettingsPreparer;
@@ -102,6 +104,26 @@ public abstract class EnvironmentAwareCommand extends Command {
         }
     }
 
+    @Override
+    protected final void beforeExecute() {
+        if (shouldConfigureLoggingWithoutConfig()) {
+            // initialize default for es.logger.level because we will not read the log4j2.properties
+            final String loggerLevel = System.getProperty("es.logger.level", Level.INFO.name());
+            final Settings settings = Settings.builder().put("logger.level", loggerLevel).build();
+            LogConfigurator.configureWithoutConfig(settings);
+        }
+    }
+
+    /**
+     * Indicate whether or not logging should be configured without reading a log4j2.properties. Most commands should do this because we do
+     * not configure logging for CLI tools. Only commands that configure logging on their own should not do this.
+     *
+     * @return true if logging should be configured without reading a log4j2.properties file
+     */
+    protected boolean shouldConfigureLoggingWithoutConfig() {
+        return true;
+    }
+
     /** Execute the command with the initialized {@link Environment}. */
     protected abstract void execute(Terminal terminal, OptionSet options, Environment env) throws Exception;
 
diff --git a/distribution/tools/plugin-cli/build.gradle b/distribution/tools/plugin-cli/build.gradle
index ae3dca9ef87..374690e65ac 100644
--- a/distribution/tools/plugin-cli/build.gradle
+++ b/distribution/tools/plugin-cli/build.gradle
@@ -21,6 +21,7 @@ apply plugin: 'elasticsearch.build'
 
 dependencies {
   provided "org.elasticsearch:elasticsearch:${version}"
+  provided "org.elasticsearch:elasticsearch-cli:${version}"
   testCompile "org.elasticsearch.test:framework:${version}"
   testCompile 'com.google.jimfs:jimfs:1.1'
   testCompile 'com.google.guava:guava:18.0'
diff --git a/qa/evil-tests/src/test/java/org/elasticsearch/cli/EvilCommandTests.java b/qa/evil-tests/src/test/java/org/elasticsearch/cli/EvilCommandTests.java
index 33d9bbe7782..7c51f8afe69 100644
--- a/qa/evil-tests/src/test/java/org/elasticsearch/cli/EvilCommandTests.java
+++ b/qa/evil-tests/src/test/java/org/elasticsearch/cli/EvilCommandTests.java
@@ -49,11 +49,11 @@ public class EvilCommandTests extends ESTestCase {
         };
         final MockTerminal terminal = new MockTerminal();
         command.main(new String[0], terminal);
-        assertNotNull(command.shutdownHookThread.get());
+        assertNotNull(command.getShutdownHookThread());
         // successful removal here asserts that the runtime hook was installed in Command#main
-        assertTrue(Runtime.getRuntime().removeShutdownHook(command.shutdownHookThread.get()));
-        command.shutdownHookThread.get().run();
-        command.shutdownHookThread.get().join();
+        assertTrue(Runtime.getRuntime().removeShutdownHook(command.getShutdownHookThread()));
+        command.getShutdownHookThread().run();
+        command.getShutdownHookThread().join();
         assertTrue(closed.get());
         final String output = terminal.getOutput();
         if (shouldThrow) {
diff --git a/settings.gradle b/settings.gradle
index d7184f647c6..3a4afaee2be 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -5,6 +5,7 @@ List projects = [
   'build-tools',
   'rest-api-spec',
   'core',
+  'core:cli',
   'docs',
   'client:rest',
   'client:rest-high-level',
diff --git a/test/framework/build.gradle b/test/framework/build.gradle
index 09382763057..558bb2c851c 100644
--- a/test/framework/build.gradle
+++ b/test/framework/build.gradle
@@ -22,6 +22,7 @@ import org.elasticsearch.gradle.precommit.PrecommitTasks;
 dependencies {
   compile "org.elasticsearch.client:elasticsearch-rest-client:${version}"
   compile "org.elasticsearch:elasticsearch:${version}"
+  compile "org.elasticsearch:elasticsearch-cli:${version}"
   compile "com.carrotsearch.randomizedtesting:randomizedtesting-runner:${versions.randomizedrunner}"
   compile "junit:junit:${versions.junit}"
   compile "org.hamcrest:hamcrest-all:${versions.hamcrest}"