diff --git a/.jshintignore b/.jshintignore
new file mode 100644
index 0000000000..de37fcc6ab
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1,23 @@
+tests/qunit/vendor/*
+tests/qunit/editor/**
+
+src/wp-content/themes/twenty{eleven,twelve,thirteen}/**
+src/wp-content/themes/twenty{fourteen,fifteen,sixteen}/js/html5.js
+src/wp-content/themes/twentyseventeen/assets/js/html5.js
+src/wp-content/themes/twentyseventeen/assets/js/jquery.scrollTo.js
+
+src/wp-includes/js/media-*
+
+src/wp-includes/js/codemirror/*.js
+src/wp-admin/js/farbtastic.js
+src/wp-includes/js/backbone*.js
+src/wp-includes/js/swfobject.js
+src/wp-includes/js/underscore*.js
+src/wp-includes/js/colorpicker.js
+src/wp-includes/js/hoverIntent.js
+src/wp-includes/js/json2.js
+src/wp-includes/js/tw-sack.js
+src/wp-includes/js/twemoji.js
+src/**/*.min.js
+
+src/wp-content/plugins/**/*.min.js
diff --git a/.jshintrc b/.jshintrc
index 1b7387f4a2..decf2015c7 100644
--- a/.jshintrc
+++ b/.jshintrc
@@ -1,7 +1,7 @@
 {
 	"boss": true,
-	"curly": true,
-	"eqeqeq": true,
+	"curly": false,
+	"eqeqeq": false,
 	"eqnull": true,
 	"es3": true,
 	"expr": true,
diff --git a/Gruntfile.js b/Gruntfile.js
index a551b308be..c03480414e 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -316,130 +316,6 @@ module.exports = function(grunt) {
 				src: []
 			}
 		},
-		jshint: {
-			options: grunt.file.readJSON('.jshintrc'),
-			grunt: {
-				src: ['Gruntfile.js']
-			},
-			tests: {
-				src: [
-					'tests/qunit/**/*.js',
-					'!tests/qunit/vendor/*',
-					'!tests/qunit/editor/**'
-				],
-				options: grunt.file.readJSON('tests/qunit/.jshintrc')
-			},
-			themes: {
-				expand: true,
-				cwd: SOURCE_DIR + 'wp-content/themes',
-				src: [
-					'twenty*/**/*.js',
-					'!twenty{eleven,twelve,thirteen}/**',
-					// Third party scripts
-					'!twenty{fourteen,fifteen,sixteen}/js/html5.js',
-					'!twentyseventeen/assets/js/html5.js',
-					'!twentyseventeen/assets/js/jquery.scrollTo.js'
-				]
-			},
-			media: {
-				src: [
-					SOURCE_DIR + 'wp-includes/js/media/**/*.js'
-				]
-			},
-			core: {
-				expand: true,
-				cwd: SOURCE_DIR,
-				src: [
-					'wp-admin/js/**/*.js',
-					'wp-includes/js/*.js',
-					// Built scripts.
-					'!wp-includes/js/media-*',
-					// WordPress scripts inside directories
-					'wp-includes/js/jquery/jquery.table-hotkeys.js',
-					'wp-includes/js/mediaelement/mediaelement-migrate.js',
-					'wp-includes/js/mediaelement/wp-mediaelement.js',
-					'wp-includes/js/mediaelement/wp-playlist.js',
-					'wp-includes/js/plupload/handlers.js',
-					'wp-includes/js/plupload/wp-plupload.js',
-					'wp-includes/js/tinymce/plugins/wordpress/plugin.js',
-					'wp-includes/js/tinymce/plugins/wp*/plugin.js',
-					// Third party scripts
-					'!wp-includes/js/codemirror/*.js',
-					'!wp-admin/js/farbtastic.js',
-					'!wp-includes/js/backbone*.js',
-					'!wp-includes/js/swfobject.js',
-					'!wp-includes/js/underscore*.js',
-					'!wp-includes/js/colorpicker.js',
-					'!wp-includes/js/hoverIntent.js',
-					'!wp-includes/js/json2.js',
-					'!wp-includes/js/tw-sack.js',
-					'!wp-includes/js/twemoji.js',
-					'!**/*.min.js'
-				],
-				// Remove once other JSHint errors are resolved
-				options: {
-					curly: false,
-					eqeqeq: false
-				},
-				// Limit JSHint's run to a single specified file:
-				//
-				//    grunt jshint:core --file=filename.js
-				//
-				// Optionally, include the file path:
-				//
-				//    grunt jshint:core --file=path/to/filename.js
-				//
-				filter: function( filepath ) {
-					var index, file = grunt.option( 'file' );
-
-					// Don't filter when no target file is specified
-					if ( ! file ) {
-						return true;
-					}
-
-					// Normalize filepath for Windows
-					filepath = filepath.replace( /\\/g, '/' );
-					index = filepath.lastIndexOf( '/' + file );
-
-					// Match only the filename passed from cli
-					if ( filepath === file || ( -1 !== index && index === filepath.length - ( file.length + 1 ) ) ) {
-						return true;
-					}
-
-					return false;
-				}
-			},
-			plugins: {
-				expand: true,
-				cwd: SOURCE_DIR + 'wp-content/plugins',
-				src: [
-					'**/*.js',
-					'!**/*.min.js'
-				],
-				// Limit JSHint's run to a single specified plugin directory:
-				//
-				//    grunt jshint:plugins --dir=foldername
-				//
-				filter: function( dirpath ) {
-					var index, dir = grunt.option( 'dir' );
-
-					// Don't filter when no target folder is specified
-					if ( ! dir ) {
-						return true;
-					}
-
-					dirpath = dirpath.replace( /\\/g, '/' );
-					index = dirpath.lastIndexOf( '/' + dir );
-
-					// Match only the folder name passed from cli
-					if ( -1 !== index ) {
-						return true;
-					}
-
-					return false;
-				}
-			}
-		},
 		jsdoc : {
 			dist : {
 				dest: 'jsdoc',
@@ -761,6 +637,9 @@ module.exports = function(grunt) {
 				files: [ '**/*.php' ],
 				tasks: [ 'phpunit:default' ]
 			}
+		},
+		exec: {
+			jshint: 'npm run jshint'
 		}
 	});
 
@@ -775,6 +654,9 @@ module.exports = function(grunt) {
 
 	// Register tasks.
 
+	// Exec task.
+	grunt.loadNpmTasks('grunt-exec');
+
 	// Webpack task.
 	grunt.loadNpmTasks( 'grunt-webpack' );
 
@@ -785,13 +667,7 @@ module.exports = function(grunt) {
 	grunt.registerTask('colors', ['sass:colors', 'postcss:colors']);
 
 	// JSHint task.
-	grunt.registerTask( 'jshint:corejs', [
-		'jshint:grunt',
-		'jshint:tests',
-		'jshint:themes',
-		'jshint:core',
-		'jshint:media'
-	] );
+	grunt.registerTask( 'jshint:corejs', ['exec:jshint'] );
 
 	grunt.registerTask( 'restapi-jsclient', [
 		'phpunit:restapi-jsclient',
diff --git a/package.json b/package.json
index 3a289ee1c7..a1afa3c8b2 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,9 @@
 	"engines": {
 		"node": "8.9.3"
 	},
+	"scripts": {
+		"jshint": "jshint Gruntfile.js tests/qunit src/wp-content/themes/twenty* src/wp-includes/js/media src/wp-includes/js/*.js src/wp-admin/js/**/*.js"
+	},
 	"author": "The WordPress Contributors",
 	"license": "GPL-2.0-or-later",
 	"devDependencies": {
@@ -21,10 +24,10 @@
 		"grunt-contrib-copy": "~1.0.0",
 		"grunt-contrib-cssmin": "~1.0.2",
 		"grunt-contrib-imagemin": "~1.0.0",
-		"grunt-contrib-jshint": "~1.0.0",
 		"grunt-contrib-qunit": "^1.2.0",
 		"grunt-contrib-uglify": "~2.0.0",
 		"grunt-contrib-watch": "~1.0.0",
+		"grunt-exec": "^3.0.0",
 		"grunt-includes": "~0.5.1",
 		"grunt-jsdoc": "^2.1.0",
 		"grunt-jsvalidate": "~0.2.2",
@@ -34,8 +37,9 @@
 		"grunt-replace": "~1.0.1",
 		"grunt-rtlcss": "~2.0.1",
 		"grunt-sass": "2.0.0",
-		"ink-docstrap": "^1.3.0",
 		"grunt-webpack": "^3.0.2",
+		"ink-docstrap": "^1.3.0",
+		"jshint": "^2.9.5",
 		"matchdep": "~1.0.0",
 		"webpack": "^3.6.0",
 		"webpack-dev-server": "^2.9.1"
diff --git a/tests/qunit/wp-admin/js/nav-menu.js b/tests/qunit/wp-admin/js/nav-menu.js
index de99e3d6f7..a7e1f37b3e 100644
--- a/tests/qunit/wp-admin/js/nav-menu.js
+++ b/tests/qunit/wp-admin/js/nav-menu.js
@@ -9,11 +9,13 @@
 	setTimeout( function() {
 		// QUnit may load this file without running it, in which case `assert`
 		// will never be set to `assertPassed` below.
-		assert && assert.equal(
-			eventsFired,
-			eventsExpected,
-			eventsExpected + ' wpNavMenu events should fire.'
-		);
+		if ( assert ) {
+			 assert.equal(
+				eventsFired,
+				eventsExpected,
+				eventsExpected + ' wpNavMenu events should fire.'
+			);
+		}
 	}, 3000 );
 
 	QUnit.test( 'Testing wpNavMenu event triggers.', function( assertPassed ) {
diff --git a/tests/qunit/wp-includes/js/tinymce/tinymce-obsolete.js b/tests/qunit/wp-includes/js/tinymce/tinymce-obsolete.js
index 9db1f9ac9d..d26ff2dddb 100644
--- a/tests/qunit/wp-includes/js/tinymce/tinymce-obsolete.js
+++ b/tests/qunit/wp-includes/js/tinymce/tinymce-obsolete.js
@@ -58,7 +58,8 @@
 		}
 
 		for ( tagName in attr ) {
-			if ( tags = html.match( new RegExp( '<' + tagName + ' [^>]+>', 'g' ) ) ) {
+			tags = html.match( new RegExp( '<' + tagName + ' [^>]+>', 'g' ) );
+			if ( tags ) {
 				for ( tag in tags ) {
 					array = attr[tagName].split(' ');
 
