diff --git a/Makefile b/Makefile
index ef2b0db49..d628ef211 100644
--- a/Makefile
+++ b/Makefile
@@ -1,12 +1,25 @@
-.PHONY: image test citest
+.PHONY: image test citest integration
 
 IMAGE_NAME ?= codeclimate/codeclimate-eslint
 
 NPM_TEST_TARGET ?= test
+NPM_INTEGRATION_TARGET ?= integration
+
+DEBUG ?= false
+ifeq ($(DEBUG),true)
+	NPM_TEST_TARGET = test.debug
+	NPM_INTEGRATION_TARGET = integration.debug
+endif
 
 image:
 	docker build --rm -t $(IMAGE_NAME) .
 
+integration: image
+	docker run -ti --rm \
+		--volume $(PWD):/code \
+		--workdir /code \
+		$(IMAGE_NAME) npm run $(NPM_INTEGRATION_TARGET)
+
 test: image
 	docker run -ti --rm \
 		--volume $(PWD):/code \
@@ -16,4 +29,4 @@ test: image
 citest:
 	docker run --rm \
 		--workdir /usr/src/app \
-		$(IMAGE_NAME) npm run test
+		$(IMAGE_NAME) sh -c "npm run test && npm run integration"
diff --git a/bin/eslint.js b/bin/eslint.js
index 83a371bc5..5d53d53e8 100755
--- a/bin/eslint.js
+++ b/bin/eslint.js
@@ -1,243 +1,7 @@
 #!/usr/src/app/bin/node_gc
 
-var CODE_DIR = "/code";
+const CODE_DIR = "/code";
 process.chdir(CODE_DIR);
 
-// Redirect `console.log` so that we are the only ones
-// writing to STDOUT
-var stdout = console.log;
-console.log = console.error;
-
-var eslint = require('../lib/eslint-patch')(require('eslint'));
-
-var CLIEngine = eslint.CLIEngine;
-var docs = eslint.docs;
-var fs = require("fs");
-var glob = require("glob");
-var options = { extensions: [".js"], ignore: true, reset: false, useEslintrc: true };
-var cli; // instantiation delayed until after options are (potentially) modified
-var debug = false;
-var BatchSanitizer = require("../lib/batch_sanitizer");
-var ignoreWarnings = false;
-var ESLINT_WARNING_SEVERITY = 1;
-var checks = require("../lib/checks");
-var validateConfig = require("../lib/validate_config");
-var computeFingerprint = require("../lib/compute_fingerprint");
-const ConfigUpgrader = require("../lib/config_upgrader");
-
-// a wrapper for emitting perf timing
-function runWithTiming(name, fn) {
-  var start = new Date()
-    , rv = fn()
-    , duration = (new Date() - start) / 1000;
-  if (debug) {
-    console.error("eslint.timing." + name + ": " + duration + "s");
-  }
-  return rv;
-}
-
-function contentBody(check) {
-  var content = docs.get(check) || "For more information visit ";
-  return content + "Source: http://eslint.org/docs/rules/\n";
-}
-
-function buildIssueJson(message, path) {
-  // ESLint doesn't emit a ruleId in the
-  // case of a fatal error (such as an invalid
-  // token)
-  var checkName = message.ruleId;
-  if(message.fatal) {
-    checkName = "fatal";
-  }
-  var line = message.line || 1;
-  var column = message.column || 1;
-
-  var issue = {
-    type: "issue",
-    categories: checks.categories(checkName),
-    check_name: checkName,
-    description: message.message,
-    content: {
-      body: contentBody(checkName)
-    },
-    location: {
-      path: path,
-      positions: {
-        begin: {
-          line: line,
-          column: column
-        },
-        end: {
-          line: line,
-          column: column
-        }
-      }
-    },
-    remediation_points: checks.remediationPoints(checkName, message, cli.getConfigForFile(path))
-  };
-
-  var fingerprint = computeFingerprint(path, checkName, message.message);
-
-  if (fingerprint) {
-    issue["fingerprint"] = fingerprint;
-  }
-
-  return JSON.stringify(issue);
-}
-
-function isFileWithMatchingExtension(file, extensions) {
-  var stats = fs.lstatSync(file);
-  var extension = "." + file.split(".").pop();
-  return (
-    stats.isFile() &&
-    !stats.isSymbolicLink()
-    && extensions.indexOf(extension) >= 0
-  );
-}
-
-function isFileIgnoredByLibrary(file) {
-  return cli.isPathIgnored(file);
-}
-
-function prunePathsWithinSymlinks(paths) {
-  // Extracts symlinked paths and filters them out, including any child paths
-  var symlinks = paths.filter(function(path) {
-    return fs.lstatSync(path).isSymbolicLink();
-  });
-
-  return paths.filter(function(path) {
-    var withinSymlink = false;
-    symlinks.forEach(function(symlink) {
-      if (path.indexOf(symlink) === 0) {
-        withinSymlink = true;
-      }
-    });
-    return !withinSymlink;
-  });
-}
-
-function inclusionBasedFileListBuilder(includePaths) {
-  // Uses glob to expand the files and directories in includePaths, filtering
-  // down to match the list of desired extensions.
-  return function(extensions) {
-    var analysisFiles = [];
-
-    includePaths.forEach(function(fileOrDirectory, i) {
-      if ((/\/$/).test(fileOrDirectory)) {
-        // if it ends in a slash, expand and push
-        var filesInThisDirectory = glob.sync(
-          fileOrDirectory + "/**/**"
-        );
-        prunePathsWithinSymlinks(filesInThisDirectory).forEach(function(file, j){
-          if (!isFileIgnoredByLibrary(file) && isFileWithMatchingExtension(file, extensions)) {
-            analysisFiles.push(file);
-          }
-        });
-      } else {
-        if (!isFileIgnoredByLibrary(fileOrDirectory) && isFileWithMatchingExtension(fileOrDirectory, extensions)) {
-          analysisFiles.push(fileOrDirectory);
-        }
-      }
-    });
-
-    return analysisFiles;
-  };
-}
-
-var buildFileList;
-runWithTiming("engineConfig", function () {
-  if (fs.existsSync("/config.json")) {
-    var engineConfig = JSON.parse(fs.readFileSync("/config.json"));
-
-    if (engineConfig.include_paths) {
-      buildFileList = inclusionBasedFileListBuilder(
-        engineConfig.include_paths
-      );
-    } else {
-      // No explicit includes, let's try with everything
-      buildFileList = inclusionBasedFileListBuilder(["./"]);
-    }
-
-    var userConfig = engineConfig.config || {};
-    if (userConfig.config) {
-      options.configFile = CODE_DIR + "/" + userConfig.config;
-      options.useEslintrc = false;
-    }
-
-    if (userConfig.extensions) {
-      options.extensions = userConfig.extensions;
-    }
-
-    if (userConfig.ignore_path) {
-      options.ignorePath = userConfig.ignore_path;
-    }
-
-    if (userConfig.ignore_warnings) {
-      ignoreWarnings = true;
-    }
-
-    if (userConfig.debug) {
-      debug = true;
-    }
-  }
-
-  cli = new CLIEngine(options);
-});
-
-var analysisFiles = runWithTiming("buildFileList", function() {
-  return buildFileList(options.extensions);
-});
-
-function analyzeFiles() {
-  var batchNum = 0
-    , batchSize = 10
-    , batchFiles
-    , batchReport
-    , sanitizedBatchFiles;
-
-  while(analysisFiles.length > 0) {
-    batchFiles = analysisFiles.splice(0, batchSize);
-    sanitizedBatchFiles = (new BatchSanitizer(batchFiles)).sanitizedFiles();
-
-    if (debug) {
-      process.stderr.write("Analyzing: " + batchFiles + "\n");
-    }
-
-    runWithTiming("analyze-batch-" + batchNum, function() {
-       batchReport = cli.executeOnFiles(sanitizedBatchFiles);
-    });
-    runWithTiming("report-batch" + batchNum, function() {
-      batchReport.results.forEach(function(result) {
-        var path = result.filePath.replace(/^\/code\//, "");
-
-        result.messages.forEach(function(message) {
-          if (ignoreWarnings && message.severity === ESLINT_WARNING_SEVERITY) { return; }
-
-          var issueJson = buildIssueJson(message, path);
-          process.stdout.write(issueJson + "\u0000\n");
-        });
-      });
-    });
-    runWithTiming("gc-batch-" + batchNum, function() {
-      batchFiles = null;
-      batchReport = null;
-      global.gc();
-    });
-
-    batchNum++;
-  }
-}
-
-if (validateConfig(options.configFile)) {
-  console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser.");
-
-  for (const line of ConfigUpgrader.upgradeInstructions(options.configFile, analysisFiles, process.cwd())) {
-    console.error(line);
-  }
-
-  analyzeFiles();
-} else {
-  console.error("No rules are configured. Make sure you have added a config file with rules enabled.");
-  console.error("See our documentation at https://docs.codeclimate.com/docs/eslint for more information.");
-  process.exit(1);
-}
+const ESLint = require("../lib/eslint");
+ESLint.run({ dir: CODE_DIR });
diff --git a/integration/empty_config/config.json b/integration/empty_config/config.json
new file mode 100644
index 000000000..a8e6e7ae6
--- /dev/null
+++ b/integration/empty_config/config.json
@@ -0,0 +1,10 @@
+{
+  "enabled": true,
+  "config": {
+    "config": "empty_config/eslintrc.yml",
+    "debug": "true"
+  },
+  "include_paths": [
+    "/usr/src/app/integration/empty_config/index.js"
+  ]
+}
diff --git a/integration/empty_config/index.js b/integration/empty_config/index.js
new file mode 100644
index 000000000..68559a432
--- /dev/null
+++ b/integration/empty_config/index.js
@@ -0,0 +1,2 @@
+function dummy() {
+}
diff --git a/integration/eslint_test.js b/integration/eslint_test.js
new file mode 100644
index 000000000..c6bcf8ccb
--- /dev/null
+++ b/integration/eslint_test.js
@@ -0,0 +1,65 @@
+const sinon = require("sinon");
+const expect = require("chai").expect;
+
+const ESLint = require('../lib/eslint');
+
+const STDOUT = console.log;
+const STDERR = console.error;
+
+describe("eslint integration", function() {
+
+  function executeConfig(configPath) {
+    ESLint.run({ dir: __dirname, configPath: `${__dirname}/${configPath}`});
+  }
+
+  beforeEach(function() {
+    console.log = sinon.spy();
+    console.error = sinon.spy();
+  });
+
+  afterEach(function() {
+    console.log = STDOUT;
+    console.error = STDERR;
+  });
+
+
+  describe("eslintrc has not supported plugins", function() {
+    it("does not raise any error", function() {
+      this.timeout(3000);
+
+      function executeUnsupportedPlugins() {
+        executeConfig("with_unsupported_plugins/config.json");
+      }
+
+      expect(executeUnsupportedPlugins).to.not.throw();
+      expect(console.log.called).to.be.ok;
+    });
+  });
+
+  describe("validating config", function() {
+    it("warns about empty config but not raise error", function() {
+      function executeEmptyConfig() {
+        executeConfig("empty_config/config.json");
+      }
+
+      expect(executeEmptyConfig).to.not.throw();
+      sinon.assert.calledWith(console.error, 'No rules are configured. Make sure you have added a config file with rules enabled.');
+    });
+  });
+
+  describe("extends plugin", function() {
+    it("loads the plugin and does not include repeated issues of not found rules", function() {
+      this.timeout(5000);
+      const output = [];
+      console.log = function(msg) {
+        output.push(msg);
+      };
+
+      executeConfig("extends_airbnb/config.json");
+
+      const ruleDefinitionIssues = output.filter(function(o) { return o.includes("Definition for rule"); });
+      expect(ruleDefinitionIssues).to.be.empty;
+    });
+  });
+
+});
diff --git a/integration/extends_airbnb/config.json b/integration/extends_airbnb/config.json
new file mode 100644
index 000000000..f1279921f
--- /dev/null
+++ b/integration/extends_airbnb/config.json
@@ -0,0 +1,10 @@
+{
+  "enabled": true,
+  "config": {
+    "config": "extends_airbnb/eslintrc.json",
+    "debug": "true"
+  },
+  "include_paths": [
+    "/usr/src/app/integration/extends_airbnb/index.js"
+  ]
+}
diff --git a/integration/extends_airbnb/eslintrc.json b/integration/extends_airbnb/eslintrc.json
new file mode 100644
index 000000000..1125e23cb
--- /dev/null
+++ b/integration/extends_airbnb/eslintrc.json
@@ -0,0 +1,5 @@
+{
+  "extends": "airbnb",
+  "parser": "babel-eslint",
+  "rules": {}
+}
diff --git a/integration/extends_airbnb/index.js b/integration/extends_airbnb/index.js
new file mode 100644
index 000000000..e69de29bb
diff --git a/integration/with_unsupported_plugins/config.json b/integration/with_unsupported_plugins/config.json
new file mode 100644
index 000000000..9f3b35f87
--- /dev/null
+++ b/integration/with_unsupported_plugins/config.json
@@ -0,0 +1,10 @@
+{
+  "enabled": true,
+  "config": {
+    "config": "with_unsupported_plugins/eslintrc.yml",
+    "debug": "false"
+  },
+  "include_paths": [
+    "/usr/src/app/integration/with_unsupported_plugins/index.js"
+  ]
+}
diff --git a/integration/with_unsupported_plugins/eslintrc.yml b/integration/with_unsupported_plugins/eslintrc.yml
new file mode 100644
index 000000000..188ce14c5
--- /dev/null
+++ b/integration/with_unsupported_plugins/eslintrc.yml
@@ -0,0 +1,21 @@
+env:
+    es6: true
+    node: true
+parserOptions:
+    sourceType: module
+plugins:
+    - node
+    - not_supported
+extends:
+    - not_valid
+    - 'plugin:invalidplugin/recommended'
+    - 'eslint:recommended'
+    - 'plugin:node/recommended'
+rules:
+    invalidplugin/rule: 1
+    node/exports-style: [error, module.exports]
+    indent: [error, 4]
+    linebreak-style: [error, unix]
+    quotes: [error, double]
+    semi: [error, always]
+    no-console: off
diff --git a/integration/with_unsupported_plugins/index.js b/integration/with_unsupported_plugins/index.js
new file mode 100644
index 000000000..68559a432
--- /dev/null
+++ b/integration/with_unsupported_plugins/index.js
@@ -0,0 +1,2 @@
+function dummy() {
+}
diff --git a/lib/docs.js b/lib/docs.js
index 0f76ad4ee..aae974b4a 100644
--- a/lib/docs.js
+++ b/lib/docs.js
@@ -7,34 +7,26 @@
 var fs = require('fs')
   , path = require('path');
 
-//------------------------------------------------------------------------------
-// Privates
-//------------------------------------------------------------------------------
+function Docs() {
 
-var docs = Object.create(null);
+  var docs = {};
+
+  function get(ruleId) {
+    return docs[ruleId];
+  }
 
-function load() {
   var docsDir = path.join(__dirname, '/docs/rules');
 
-  fs.readdirSync(docsDir).forEach(function(file) {
+  fs.existsSync(docsDir) && fs.readdirSync(docsDir).forEach(function(file) {
     var content = fs.readFileSync(docsDir + '/' + file, 'utf8');
 
     // Remove the .md extension from the filename
     docs[file.slice(0, -3)] = content;
   });
-}
-
-//------------------------------------------------------------------------------
-// Public Interface
-//------------------------------------------------------------------------------
 
-exports.get = function(ruleId) {
-  return docs[ruleId];
-};
-
-//------------------------------------------------------------------------------
-// Initialization
-//------------------------------------------------------------------------------
+  return {
+    get: get
+  };
+}
 
-// loads existing docs
-load();
+module.exports = Docs;
diff --git a/lib/empty-plugin.js b/lib/empty-plugin.js
new file mode 100644
index 000000000..b8f05e40a
--- /dev/null
+++ b/lib/empty-plugin.js
@@ -0,0 +1,6 @@
+module.exports = {
+  rules: {},
+  configs: {
+    recommended: {}
+  }
+};
diff --git a/lib/eslint-patch.js b/lib/eslint-patch.js
index f2677ca98..f288d29d7 100644
--- a/lib/eslint-patch.js
+++ b/lib/eslint-patch.js
@@ -1,33 +1,43 @@
-'use strict';
-var meld = require('meld');
-var docs = require('./docs');
-var Config = require("eslint/lib/config");
-var ConfigUpgrader = require('./config_upgrader');
-
-var supportedPlugins = ['react', 'babel'];
-
-module.exports = function patcher(eslint) {
-
-  meld.around(eslint.CLIEngine, 'loadPlugins', function(joinPoint) {
-    var pluginNames = joinPoint.args[0];
-    var filteredPluginNames = pluginNames.filter(function(pluginName) {
-      return supportedPlugins.indexOf(pluginName) >= 0;
-    });
-    return joinPoint.proceed(filteredPluginNames);
-  });
-
-  meld.around(eslint.CLIEngine, 'addPlugin', function() {
-    return;
-  });
-
-  // meld.around(eslint.CLIEngine.Config, 'loadPackage', function(joinPoint) {
-  //   var filePath = joinPoint.args[0];
-  //   if (filePath.match(/^eslint-config-airbnb.*/)) {
-  //     return joinPoint.proceed();
-  //   } else {
-  //     return {};
-  //   }
-  // });
+"use strict";
+
+const Plugins = require("eslint/lib/config/plugins");
+const ModuleResolver = require("eslint/lib/util/module-resolver");
+
+const ConfigFile = require("eslint/lib/config/config-file");
+
+const Config = require("eslint/lib/config");
+const ConfigUpgrader = require("./config_upgrader");
+
+module.exports = function patch() {
+  const skippedModules = [];
+  function warnModuleNotSupported(name) {
+    if(!skippedModules.includes(name)) {
+      skippedModules.push(name);
+      console.error(`Module not supported: ${name}`);
+    }
+  }
+
+  const resolve = ModuleResolver.prototype.resolve;
+  ModuleResolver.prototype.resolve = function(name, path) {
+    try {
+      return resolve.apply(this, [name, path]);
+    } catch(e) {
+      warnModuleNotSupported(name);
+      return `${__dirname}/empty-plugin.js`;
+    }
+  };
+
+  Plugins.loadAll = function(pluginNames) {
+    for(const name of pluginNames) {
+      try {
+
+        Plugins.load(name);
+
+      } catch(e) {
+        warnModuleNotSupported(`eslint-plugin-${name}`);
+      }
+    }
+  };
 
   const originalGetConfig = Config.prototype.getConfig;
   Config.prototype.getConfig = function(filePath) {
@@ -37,7 +47,5 @@ module.exports = function patcher(eslint) {
     return configUpgrader.upgrade(originalConfig);
   };
 
-  eslint.docs = docs;
-
-  return eslint;
+  return require('eslint');
 };
diff --git a/lib/eslint.js b/lib/eslint.js
new file mode 100755
index 000000000..689081ec8
--- /dev/null
+++ b/lib/eslint.js
@@ -0,0 +1,240 @@
+#!/usr/src/app/bin/node_gc
+
+const fs = require("fs");
+const glob = require("glob");
+
+const eslint = require('./eslint-patch')();
+const docs = require('./docs')();
+const BatchSanitizer = require("./batch_sanitizer");
+const checks = require("./checks");
+const validateConfig = require("./validate_config");
+const computeFingerprint = require("./compute_fingerprint");
+const ConfigUpgrader = require("./config_upgrader");
+
+const CLIEngine = eslint.CLIEngine;
+const options = { extensions: [".js"], ignore: true, reset: false, useEslintrc: true };
+
+function run(runOptions) {
+  var configPath = runOptions.configPath || "/config.json";
+  var codeDir = runOptions.dir || "/code";
+
+  var cli; // instantiation delayed until after options are (potentially) modified
+  var debug = false;
+  var ignoreWarnings = false;
+  var ESLINT_WARNING_SEVERITY = 1;
+
+  // a wrapper for emitting perf timing
+  function runWithTiming(name, fn) {
+    const start = new Date();
+    const result = fn();
+
+    if (debug) {
+      const duration = (new Date() - start) / 1000;
+      console.error("eslint.timing." + name + ": " + duration + "s");
+    }
+
+    return result;
+  }
+
+  function contentBody(check) {
+    var content = docs.get(check) || "For more information visit ";
+    return content + "Source: http://eslint.org/docs/rules/\n";
+  }
+
+  function buildIssueJson(message, path) {
+    // ESLint doesn't emit a ruleId in the
+    // case of a fatal error (such as an invalid
+    // token)
+    var checkName = message.ruleId;
+    if(message.fatal) {
+      checkName = "fatal";
+    }
+    var line = message.line || 1;
+    var column = message.column || 1;
+
+    var issue = {
+      type: "issue",
+      categories: checks.categories(checkName),
+      check_name: checkName,
+      description: message.message,
+      content: {
+        body: contentBody(checkName)
+      },
+      location: {
+        path: path,
+        positions: {
+          begin: {
+            line: line,
+            column: column
+          },
+          end: {
+            line: line,
+            column: column
+          }
+        }
+      },
+      remediation_points: checks.remediationPoints(checkName, message, cli.getConfigForFile(path))
+    };
+
+    var fingerprint = computeFingerprint(path, checkName, message.message);
+
+    if (fingerprint) {
+      issue["fingerprint"] = fingerprint;
+    }
+
+    return JSON.stringify(issue);
+  }
+
+  function isFileWithMatchingExtension(file, extensions) {
+    var stats = fs.lstatSync(file);
+    var extension = "." + file.split(".").pop();
+    return (
+      stats.isFile() &&
+      !stats.isSymbolicLink()
+      && extensions.indexOf(extension) >= 0
+    );
+  }
+
+  function isFileIgnoredByLibrary(file) {
+    return cli.isPathIgnored(file);
+  }
+
+  function prunePathsWithinSymlinks(paths) {
+    // Extracts symlinked paths and filters them out, including any child paths
+    var symlinks = paths.filter(function(path) {
+      return fs.lstatSync(path).isSymbolicLink();
+    });
+
+    return paths.filter(function(path) {
+      var withinSymlink = false;
+      symlinks.forEach(function(symlink) {
+        if (path.indexOf(symlink) === 0) {
+          withinSymlink = true;
+        }
+      });
+      return !withinSymlink;
+    });
+  }
+
+  function inclusionBasedFileListBuilder(includePaths) {
+    // Uses glob to expand the files and directories in includePaths, filtering
+    // down to match the list of desired extensions.
+    return function(extensions) {
+      var analysisFiles = [];
+
+      includePaths.forEach(function(fileOrDirectory, i) {
+        if ((/\/$/).test(fileOrDirectory)) {
+          // if it ends in a slash, expand and push
+          var filesInThisDirectory = glob.sync(
+            fileOrDirectory + "/**/**"
+          );
+          prunePathsWithinSymlinks(filesInThisDirectory).forEach(function(file, j){
+            if (!isFileIgnoredByLibrary(file) && isFileWithMatchingExtension(file, extensions)) {
+              analysisFiles.push(file);
+            }
+          });
+        } else {
+          if (!isFileIgnoredByLibrary(fileOrDirectory) && isFileWithMatchingExtension(fileOrDirectory, extensions)) {
+            analysisFiles.push(fileOrDirectory);
+          }
+        }
+      });
+
+      return analysisFiles;
+    };
+  }
+
+  function overrideOptions(userConfig) {
+    if (userConfig.config) {
+      options.configFile = codeDir + "/" + userConfig.config;
+      options.useEslintrc = false;
+    }
+
+    if (userConfig.extensions) {
+      options.extensions = userConfig.extensions;
+    }
+
+    if (userConfig.ignore_path) {
+      options.ignorePath = userConfig.ignore_path;
+    }
+
+    ignoreWarnings = !!userConfig.ignore_warnings;
+    debug = !!userConfig.debug;
+  }
+
+  // No explicit includes, let's try with everything
+  var buildFileList = inclusionBasedFileListBuilder(["./"]);
+
+  runWithTiming("engineConfig", function () {
+    if (fs.existsSync(configPath)) {
+      var engineConfig = JSON.parse(fs.readFileSync(configPath));
+
+      if (engineConfig.include_paths) {
+        buildFileList = inclusionBasedFileListBuilder(engineConfig.include_paths);
+      }
+
+      overrideOptions(engineConfig.config || {});
+    }
+
+    cli = new CLIEngine(options);
+  });
+
+  var analysisFiles = runWithTiming("buildFileList", function() {
+    return buildFileList(options.extensions);
+  });
+
+  function analyzeFiles() {
+    var batchNum = 0
+      , batchSize = 10
+      , batchFiles
+      , batchReport
+      , sanitizedBatchFiles;
+
+    while(analysisFiles.length > 0) {
+      batchFiles = analysisFiles.splice(0, batchSize);
+      sanitizedBatchFiles = (new BatchSanitizer(batchFiles)).sanitizedFiles();
+
+      if (debug) {
+        console.error("Analyzing: " + batchFiles);
+      }
+
+      runWithTiming("analyze-batch-" + batchNum, function() {
+        batchReport = cli.executeOnFiles(sanitizedBatchFiles);
+      });
+      runWithTiming("report-batch" + batchNum, function() {
+        batchReport.results.forEach(function(result) {
+          var path = result.filePath.replace(/^\/code\//, "");
+
+          result.messages.forEach(function(message) {
+            if (ignoreWarnings && message.severity === ESLINT_WARNING_SEVERITY) { return; }
+
+            var issueJson = buildIssueJson(message, path);
+            console.log(issueJson + "\u0000\n");
+          });
+        });
+      });
+      runWithTiming("gc-batch-" + batchNum, function() {
+        batchFiles = null;
+        batchReport = null;
+        global.gc();
+      });
+
+      batchNum++;
+    }
+  }
+
+  if (validateConfig(options.configFile)) {
+    console.error("ESLint is running with the " + cli.getConfigForFile(null).parser + " parser.");
+
+    for (const line of ConfigUpgrader.upgradeInstructions(options.configFile, analysisFiles, process.cwd())) {
+      console.error(line);
+    }
+
+    analyzeFiles();
+  } else {
+    console.error("No rules are configured. Make sure you have added a config file with rules enabled.");
+    console.error("See our documentation at https://docs.codeclimate.com/docs/eslint for more information.");
+  }
+}
+
+module.exports = { run };
diff --git a/package.json b/package.json
index 5787f41ba..7c9147a5d 100644
--- a/package.json
+++ b/package.json
@@ -59,8 +59,7 @@
     "eslint-plugin-vue": "^2.0.1",
     "eslint-plugin-xogroup": "^1.2.0",
     "glob": "^7.0.6",
-    "prettier": "^0.22.0",
-    "meld": "^1.3.2"
+    "prettier": "^0.22.0"
   },
   "devDependencies": {
     "chai": "^3.5.0",
@@ -69,8 +68,10 @@
     "temp": "^0.8.3"
   },
   "scripts": {
-    "test": "mocha test",
-    "test.debug": "mocha debug test"
+    "integration": "mocha -gc integration",
+    "integration.debug": "mocha -gc debug integration",
+    "test": "mocha -gc test",
+    "test.debug": "mocha -gc debug test"
   },
   "engine": "node >= 0.12.4"
 }
diff --git a/test/eslint-patch_test.js b/test/eslint-patch_test.js
new file mode 100644
index 000000000..d126becb8
--- /dev/null
+++ b/test/eslint-patch_test.js
@@ -0,0 +1,89 @@
+const expect = require("chai").expect;
+const sinon = require("sinon");
+
+const Plugins = require("eslint/lib/config/plugins");
+const ModuleResolver = require("eslint/lib/util/module-resolver");
+const eslintPatch = require("../lib/eslint-patch");
+
+describe("eslint-patch", function() {
+  describe("patch", function() {
+    let loadAll;
+
+    before(function() {
+      loadAll = Plugins.loadAll;
+    });
+
+    after(function() {
+      Plugins.loadAll = loadAll;
+    });
+
+    it("intercepts plugins", function() {
+      eslintPatch();
+      expect(loadAll).to.not.equal(Plugins.loadAll, "Plugins.loadAll is not patched");
+    });
+  });
+
+  describe("Plugins.loadAll", function() {
+    before(function() {
+      eslintPatch();
+    });
+
+    it("delegates each plugin to be loaded", function () {
+      Plugins.load = sinon.spy();
+
+      Plugins.loadAll([ "jasmine", "mocha"  ]);
+
+      expect(Plugins.load.calledWith("jasmine")).to.be.true;
+      expect(Plugins.load.calledWith("mocha")).to.be.true;
+    });
+
+    it("only warns not supported once", function () {
+      console.error = sinon.spy();
+      Plugins.load = sinon.stub().throws();
+
+      Plugins.loadAll([ "node" ]);
+      Plugins.loadAll([ "node" ]);
+
+      sinon.assert.calledOnce(console.error);
+      sinon.assert.calledWith(console.error, "Module not supported: eslint-plugin-node");
+    });
+
+    it("does not raise exception for unsupported plugins", function() {
+      Plugins.getAll = sinon.stub().returns([]);
+      Plugins.load = sinon.stub().throws();
+
+      function loadPlugin() {
+        Plugins.loadAll([ "unsupported-plugin" ]);
+      }
+
+      expect(loadPlugin).to.not.throw();
+    });
+  });
+
+  describe("loading extends configuration", function() {
+    it("patches module resolver", function() {
+      const resolve = ModuleResolver.prototype.resolve;
+
+      eslintPatch();
+      expect(ModuleResolver.prototype.resolve).to.not.eql(resolve);
+    });
+
+    it("returns fake config for skipped modules", function() {
+      eslintPatch();
+      Plugins.loadAll(['invalidplugin']);
+      expect(new ModuleResolver().resolve('eslint-plugin-invalidplugin')).to.match(/.+empty-plugin.js/);
+    });
+
+    it("does not warn user repeatedly about not supported modules", function() {
+      console.error = sinon.spy();
+      eslintPatch();
+
+      for(var i=0; i<3; i++) {
+        new ModuleResolver().resolve('eslint-plugin-bogus');
+      }
+
+      expect(console.error.callCount).to.eql(1);
+    });
+  });
+
+});
diff --git a/yarn.lock b/yarn.lock
index b26a1900c..d728a389e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1423,10 +1423,6 @@ lru-cache@2:
   version "2.7.3"
   resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952"
 
-meld@^1.3.2:
-  version "1.3.2"
-  resolved "https://registry.yarnpkg.com/meld/-/meld-1.3.2.tgz#8c3235fb5001b8796f8768818e9e4563b0de8066"
-
 minimatch@0.3:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-0.3.0.tgz#275d8edaac4f1bb3326472089e7949c8394699dd"