diff --git a/Makefile b/Makefile
index ef2b0db49..8820fb870 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) npm run test integration
diff --git a/bin/eslint.js b/bin/eslint.js
index 1571fc352..5dc2583a6 100755
--- a/bin/eslint.js
+++ b/bin/eslint.js
@@ -1,244 +1,9 @@
 #!/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;
+const ESLint = require("../lib/eslint");
+const exitCode = ESLint.run({ dir: CODE_DIR });
 
-
-var eslint = require('../lib/eslint-patch')();
-
-var CLIEngine = eslint.CLIEngine;
-var docs = require('../lib/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);
-}
+process.exit(exitCode);
diff --git a/integration/eslint_test.js b/integration/eslint_test.js
new file mode 100644
index 000000000..e8c711ff3
--- /dev/null
+++ b/integration/eslint_test.js
@@ -0,0 +1,39 @@
+const sinon = require("sinon");
+const expect = require("chai").expect;
+
+global.gc = function(){};
+
+const STDOUT = console.log;
+const STDERR = console.error;
+
+describe("eslint integration", function() {
+  describe("eslintrc has not supported plugins", function() {
+    before(function() {
+      console.log = sinon.spy();
+      console.error = sinon.spy();
+    });
+
+    after(function() {
+      console.log = STDOUT;
+      console.error = STDERR;
+    });
+
+    it("does not raise any error", function() {
+      this.timeout(3000);
+
+      var consoleStub = {
+        log: sinon.spy(),
+        error: sinon.spy()
+      };
+
+      function execute() {
+        const ESLint = require('../lib/eslint');
+        ESLint.run({ dir: __dirname, configPath: `${__dirname}/with_unsupported_plugins/config.json`});
+      }
+
+      expect(execute).to.not.throw();
+      expect(console.log.called).to.be.ok;
+    });
+  });
+
+});
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 14eda0194..aae974b4a 100644
--- a/lib/docs.js
+++ b/lib/docs.js
@@ -9,7 +9,7 @@ var fs = require('fs')
 
 function Docs() {
 
-  var docs = Object.create(null);
+  var docs = {};
 
   function get(ruleId) {
     return docs[ruleId];
@@ -17,7 +17,7 @@ function Docs() {
 
   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
diff --git a/lib/eslint-patch.js b/lib/eslint-patch.js
index 62114876a..fd6d82633 100644
--- a/lib/eslint-patch.js
+++ b/lib/eslint-patch.js
@@ -12,10 +12,10 @@ module.exports = function patch() {
 
   const skippedModules = [];
   function warnModuleNotSupported(name) {
-      if(skippedModules.indexOf(name) < 0) {
-        skippedModules.push(name);
-        console.error(`Module not supported: ${name}`);
-      }
+    if(skippedModules.indexOf(name) < 0) {
+      skippedModules.push(name);
+      console.error(`Module not supported: ${name}`);
+    }
   }
 
   const resolve = ModuleResolver.prototype.resolve;
diff --git a/lib/eslint.js b/lib/eslint.js
new file mode 100755
index 000000000..658320983
--- /dev/null
+++ b/lib/eslint.js
@@ -0,0 +1,243 @@
+#!/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.");
+    return 1;
+  }
+
+  return 0;
+}
+
+module.exports = { run };
diff --git a/package.json b/package.json
index 1d28841fb..1fc0b340d 100644
--- a/package.json
+++ b/package.json
@@ -68,6 +68,8 @@
     "temp": "^0.8.3"
   },
   "scripts": {
+    "integration": "mocha integration",
+    "integration.debug": "mocha debug integration",
     "test": "mocha test",
     "test.debug": "mocha debug test"
   },