diff --git a/@commitlint/cli/fixtures/issue-prefixes/commitlint.config.js b/@commitlint/cli/fixtures/issue-prefixes/commitlint.config.js
new file mode 100644
index 0000000000..c07ee41eb6
--- /dev/null
+++ b/@commitlint/cli/fixtures/issue-prefixes/commitlint.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+	rules: {
+		'references-empty': [2, 'never']
+	},
+	parserPreset: {
+		parserOpts: {
+			issuePrefixes: ['REF-']
+		}
+	}
+};
diff --git a/@commitlint/cli/fixtures/parser-preset/commitlint.config.js b/@commitlint/cli/fixtures/parser-preset/commitlint.config.js
index eebcf63055..c37964c6f5 100644
--- a/@commitlint/cli/fixtures/parser-preset/commitlint.config.js
+++ b/@commitlint/cli/fixtures/parser-preset/commitlint.config.js
@@ -1,7 +1,5 @@
 module.exports = {
-	parserOpts: {
-		parserPreset: './parser-preset'
-	},
+	parserPreset: './parser-preset',
 	rules: {
 		'type-enum': [2, 'always', ['type']],
 		'scope-enum': [2, 'always', ['scope']],
diff --git a/@commitlint/cli/src/cli.js b/@commitlint/cli/src/cli.js
index ece9b3cbd9..2b0b9d8c10 100755
--- a/@commitlint/cli/src/cli.js
+++ b/@commitlint/cli/src/cli.js
@@ -96,17 +96,17 @@ async function main(options) {
 		throw err;
 	}
 
-	return Promise.all(
-		messages.map(async message => {
-			const loaded = await core.load(getSeed(flags), {cwd: flags.cwd});
-			const parserOpts = selectParserOpts(loaded.parserPreset);
-			const opts = parserOpts ? {parserOpts} : {parserOpts: {}};
+	const loaded = await core.load(getSeed(flags), {cwd: flags.cwd});
+	const parserOpts = selectParserOpts(loaded.parserPreset);
+	const opts = parserOpts ? {parserOpts} : {parserOpts: {}};
 
-			// Strip comments if reading from `.git/COMMIT_EDIT_MSG`
-			if (range.edit) {
-				opts.parserOpts.commentChar = '#';
-			}
+	// Strip comments if reading from `.git/COMMIT_EDIT_MSG`
+	if (range.edit) {
+		opts.parserOpts.commentChar = '#';
+	}
 
+	return Promise.all(
+		messages.map(async message => {
 			const report = await core.lint(message, loaded.rules, opts);
 			const formatted = core.format(report, {color: flags.color});
 
@@ -182,13 +182,11 @@ function selectParserOpts(parserPreset) {
 		return undefined;
 	}
 
-	const opts = parserPreset.opts;
-
-	if (typeof opts !== 'object') {
+	if (typeof parserPreset.parserOpts !== 'object') {
 		return undefined;
 	}
 
-	return opts.parserOpts;
+	return parserPreset.parserOpts;
 }
 
 // Catch unhandled rejections globally
diff --git a/@commitlint/cli/src/cli.test.js b/@commitlint/cli/src/cli.test.js
index 4567afdf13..9ca1b98ae2 100644
--- a/@commitlint/cli/src/cli.test.js
+++ b/@commitlint/cli/src/cli.test.js
@@ -182,6 +182,12 @@ test('should handle --amend with signoff', async () => {
 	await execa('git', ['commit', '--amend', '--no-edit'], {cwd});
 });
 
+test('should handle linting with issue prefixes', async t => {
+	const cwd = await git.bootstrap('fixtures/issue-prefixes');
+	const actual = await cli([], {cwd})('foobar REF-1');
+	t.is(actual.code, 0);
+});
+
 async function writePkg(payload, options) {
 	const pkgPath = path.join(options.cwd, 'package.json');
 	const pkg = JSON.parse(await sander.readFile(pkgPath));
diff --git a/@commitlint/core/src/lint.js b/@commitlint/core/src/lint.js
index f0d375fe5c..5987f91fff 100644
--- a/@commitlint/core/src/lint.js
+++ b/@commitlint/core/src/lint.js
@@ -1,3 +1,4 @@
+import util from 'util';
 import isIgnored from '@commitlint/is-ignored';
 import parse from '@commitlint/parse';
 import implementations from '@commitlint/rules';
@@ -16,6 +17,84 @@ export default async (message, rules = {}, opts = {}) => {
 	// Parse the commit message
 	const parsed = await parse(message, undefined, opts.parserOpts);
 
+	// Find invalid rules configs
+	const missing = Object.keys(rules).filter(
+		name => typeof implementations[name] !== 'function'
+	);
+
+	if (missing.length > 0) {
+		const names = Object.keys(implementations);
+		throw new RangeError(
+			`Found missing rule names: ${missing.join(
+				', '
+			)}. Supported rule names are: ${names.join(', ')}`
+		);
+	}
+
+	const invalid = entries(rules)
+		.map(([name, config]) => {
+			if (!Array.isArray(config)) {
+				return new Error(
+					`config for rule ${name} must be array, received ${util.inspect(
+						config
+					)} of type ${typeof config}`
+				);
+			}
+
+			const [level, when] = config;
+
+			if (typeof level !== 'number' || isNaN(level)) {
+				return new Error(
+					`level for rule ${name} must be number, received ${util.inspect(
+						level
+					)} of type ${typeof level}`
+				);
+			}
+
+			if (level === 0 && config.length === 1) {
+				return null;
+			}
+
+			if (config.length !== 2 && config.length !== 3) {
+				return new Error(
+					`config for rule ${name} must be 2 or 3 items long, received ${util.inspect(
+						config
+					)} of length ${config.length}`
+				);
+			}
+
+			if (level < 0 || level > 2) {
+				return new RangeError(
+					`level for rule ${name} must be between 0 and 2, received ${util.inspect(
+						level
+					)}`
+				);
+			}
+
+			if (typeof when !== 'string') {
+				return new Error(
+					`condition for rule ${name} must be string, received ${util.inspect(
+						when
+					)} of type ${typeof when}`
+				);
+			}
+
+			if (when !== 'never' && when !== 'always') {
+				return new Error(
+					`condition for rule ${name} must be "always" or "never", received ${util.inspect(
+						when
+					)}`
+				);
+			}
+
+			return null;
+		})
+		.filter(item => item instanceof Error);
+
+	if (invalid.length > 0) {
+		throw new Error(invalid.map(i => i.message).join('\n'));
+	}
+
 	// Validate against all rules
 	const results = entries(rules)
 		.filter(entry => {
@@ -32,6 +111,7 @@ export default async (message, rules = {}, opts = {}) => {
 			}
 
 			const rule = implementations[name];
+
 			const [valid, message] = rule(parsed, when, value);
 
 			return {
diff --git a/@commitlint/core/src/lint.test.js b/@commitlint/core/src/lint.test.js
index dd87c166bd..93cd7a36e6 100644
--- a/@commitlint/core/src/lint.test.js
+++ b/@commitlint/core/src/lint.test.js
@@ -52,3 +52,133 @@ test('positive on stub message and opts', async t => {
 	);
 	t.true(actual.valid);
 });
+
+test('throws for invalid rule names', async t => {
+	const error = await t.throws(
+		lint('foo', {foo: [2, 'always'], bar: [1, 'never']})
+	);
+
+	t.is(error.message.indexOf('Found missing rule names: foo, bar'), 0);
+});
+
+test('throws for invalid rule config', async t => {
+	const error = await t.throws(
+		lint('type(scope): foo', {
+			'type-enum': 1,
+			'scope-enum': {0: 2, 1: 'never', 2: ['foo'], length: 3}
+		})
+	);
+
+	t.true(error.message.indexOf('type-enum must be array') > -1);
+	t.true(error.message.indexOf('scope-enum must be array') > -1);
+});
+
+test('allows disable shorthand', async t => {
+	await t.notThrows(lint('foo', {'type-enum': [0], 'scope-enum': [0]}));
+});
+
+test('throws for rule with invalid length', async t => {
+	const error = await t.throws(
+		lint('type(scope): foo', {'scope-enum': [1, 2, 3, 4]})
+	);
+
+	t.true(error.message.indexOf('scope-enum must be 2 or 3 items long') > -1);
+});
+
+test('throws for rule with invalid level', async t => {
+	const error = await t.throws(
+		lint('type(scope): foo', {
+			'type-enum': ['2', 'always'],
+			'header-max-length': [{}, 'always']
+		})
+	);
+
+	t.true(error.message.indexOf('rule type-enum must be number') > -1);
+	t.true(error.message.indexOf('rule type-enum must be number') > -1);
+});
+
+test('throws for rule with out of range level', async t => {
+	const error = await t.throws(
+		lint('type(scope): foo', {
+			'type-enum': [-1, 'always'],
+			'header-max-length': [3, 'always']
+		})
+	);
+
+	t.true(error.message.indexOf('rule type-enum must be between 0 and 2') > -1);
+	t.true(error.message.indexOf('rule type-enum must be between 0 and 2') > -1);
+});
+
+test('throws for rule with invalid condition', async t => {
+	const error = await t.throws(
+		lint('type(scope): foo', {
+			'type-enum': [1, 2],
+			'header-max-length': [1, {}]
+		})
+	);
+
+	t.true(error.message.indexOf('type-enum must be string') > -1);
+	t.true(error.message.indexOf('header-max-length must be string') > -1);
+});
+
+test('throws for rule with out of range condition', async t => {
+	const error = await t.throws(
+		lint('type(scope): foo', {
+			'type-enum': [1, 'foo'],
+			'header-max-length': [1, 'bar']
+		})
+	);
+
+	t.true(error.message.indexOf('type-enum must be "always" or "never"') > -1);
+	t.true(
+		error.message.indexOf('header-max-length must be "always" or "never"') > -1
+	);
+});
+
+test('succeds for issue', async t => {
+	const report = await lint('somehting #1', {
+		'references-empty': [2, 'never']
+	});
+
+	t.true(report.valid);
+});
+
+test('fails for issue', async t => {
+	const report = await lint('somehting #1', {
+		'references-empty': [2, 'always']
+	});
+
+	t.false(report.valid);
+});
+
+test('succeds for custom issue prefix', async t => {
+	const report = await lint(
+		'somehting REF-1',
+		{
+			'references-empty': [2, 'never']
+		},
+		{
+			parserOpts: {
+				issuePrefixes: ['REF-']
+			}
+		}
+	);
+
+	t.true(report.valid);
+});
+
+test('fails for custom issue prefix', async t => {
+	const report = await lint(
+		'somehting #1',
+		{
+			'references-empty': [2, 'never']
+		},
+		{
+			parserOpts: {
+				issuePrefixes: ['REF-']
+			}
+		}
+	);
+
+	t.false(report.valid);
+});
diff --git a/@commitlint/core/src/load.js b/@commitlint/core/src/load.js
index 59cee9c6b1..0750b2e8f3 100644
--- a/@commitlint/core/src/load.js
+++ b/@commitlint/core/src/load.js
@@ -26,7 +26,7 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => {
 		config.parserPreset = {
 			name: config.parserPreset,
 			path: resolvedParserPreset,
-			opts: require(resolvedParserPreset)
+			parserOpts: (await require(resolvedParserPreset)).parserOpts
 		};
 	}
 
@@ -38,13 +38,14 @@ export default async (seed = {}, options = {cwd: process.cwd()}) => {
 	});
 
 	const preset = valid(mergeWith(extended, config, w));
-
 	// Await parser-preset if applicable
 	if (
 		typeof preset.parserPreset === 'object' &&
-		typeof preset.parserPreset.opts === 'object'
+		typeof preset.parserPreset.parserOpts === 'object' &&
+		typeof preset.parserPreset.parserOpts.then === 'function'
 	) {
-		preset.parserPreset.opts = await preset.parserPreset.opts;
+		preset.parserPreset.parserOpts = (await preset.parserPreset
+			.parserOpts).parserOpts;
 	}
 
 	// Execute rule config functions if needed
diff --git a/@commitlint/core/src/load.test.js b/@commitlint/core/src/load.test.js
index 3fa9da29bc..3d9569aa09 100644
--- a/@commitlint/core/src/load.test.js
+++ b/@commitlint/core/src/load.test.js
@@ -24,12 +24,9 @@ test('uses seed with parserPreset', async t => {
 		},
 		{cwd}
 	);
-
 	t.is(actual.name, './conventional-changelog-custom');
-	t.deepEqual(actual.opts, {
-		parserOpts: {
-			headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/
-		}
+	t.deepEqual(actual.parserOpts, {
+		headerPattern: /^(\w*)(?:\((.*)\))?-(.*)$/
 	});
 });
 
@@ -134,25 +131,19 @@ test('recursive extends with package.json file', async t => {
 test('parser preset overwrites completely instead of merging', async t => {
 	const cwd = await git.bootstrap('fixtures/parser-preset-override');
 	const actual = await load({}, {cwd});
-
 	t.is(actual.parserPreset.name, './custom');
-	t.is(typeof actual.parserPreset.opts, 'object');
-	t.deepEqual(actual.parserPreset.opts, {
-		b: 'b',
-		parserOpts: {
-			headerPattern: /.*/
-		}
+	t.deepEqual(actual.parserPreset.parserOpts, {
+		headerPattern: /.*/
 	});
 });
 
 test('recursive extends with parserPreset', async t => {
 	const cwd = await git.bootstrap('fixtures/recursive-parser-preset');
 	const actual = await load({}, {cwd});
-
 	t.is(actual.parserPreset.name, './conventional-changelog-custom');
-	t.is(typeof actual.parserPreset.opts, 'object');
+	t.is(typeof actual.parserPreset.parserOpts, 'object');
 	t.deepEqual(
-		actual.parserPreset.opts.parserOpts.headerPattern,
+		actual.parserPreset.parserOpts.headerPattern,
 		/^(\w*)(?:\((.*)\))?-(.*)$/
 	);
 });
diff --git a/@commitlint/parse/README.md b/@commitlint/parse/README.md
new file mode 100644
index 0000000000..6914b3be8d
--- /dev/null
+++ b/@commitlint/parse/README.md
@@ -0,0 +1,29 @@
+> Parse commit messages to structured data
+
+# @commitlint/parse
+
+## Install
+
+```
+npm install --save @commitlint/parse
+```
+
+## Use
+
+```js
+const parse = require('@commitlint/parse');
+```
+
+## API
+
+### parse(message: string, parser: Function, parserOpts: Object)
+
+* **message**: Commit message to parser
+* **parser**: Sync parser function to use. Defaults to `sync` of `conventional-commits-parser`
+* **parserOpts**: Options to pass to `parser`
+  ```js
+  {
+    commentChar: null, // character indicating comment lines
+    issuePrefixes: ['#'] // prefix characters for issue references
+  }
+  ```
diff --git a/@commitlint/parse/src/index.test.js b/@commitlint/parse/src/index.test.js
index 633898643b..f4afab09b8 100644
--- a/@commitlint/parse/src/index.test.js
+++ b/@commitlint/parse/src/index.test.js
@@ -140,3 +140,20 @@ test('parses references leading subject', async t => {
 	const {references: [actual]} = await parse(message, undefined, opts);
 	t.is(actual.issue, '1');
 });
+
+test('parses custom references', async t => {
+	const message = '#1 some subject PREFIX-2';
+	const {references} = await parse(message, undefined, {
+		issuePrefixes: ['PREFIX-']
+	});
+
+	t.falsy(references.find(ref => ref.issue === '1'));
+	t.deepEqual(references.find(ref => ref.issue === '2'), {
+		action: null,
+		issue: '2',
+		owner: null,
+		prefix: 'PREFIX-',
+		raw: '#1 some subject PREFIX-2',
+		repository: null
+	});
+});
diff --git a/@commitlint/resolve-extends/src/index.js b/@commitlint/resolve-extends/src/index.js
index 891f134ebb..40bb74bd8f 100644
--- a/@commitlint/resolve-extends/src/index.js
+++ b/@commitlint/resolve-extends/src/index.js
@@ -48,13 +48,12 @@ function loadExtends(config = {}, context = {}) {
 			typeof c.parserPreset === 'string'
 		) {
 			const resolvedParserPreset = resolveFrom(cwd, c.parserPreset);
-
 			const parserPreset = {
 				name: c.parserPreset,
 				path: `./${path.relative(process.cwd(), resolvedParserPreset)}`
 					.split(path.sep)
 					.join('/'),
-				opts: require(resolvedParserPreset)
+				parserOpts: require(resolvedParserPreset)
 			};
 
 			ctx.parserPreset = parserPreset;
diff --git a/@commitlint/rules/src/references-empty.test.js b/@commitlint/rules/src/references-empty.test.js
index a491600b32..a414362999 100644
--- a/@commitlint/rules/src/references-empty.test.js
+++ b/@commitlint/rules/src/references-empty.test.js
@@ -7,7 +7,8 @@ const messages = {
 	plain: 'foo: bar',
 	comment: 'foo: baz\n#1 Comment',
 	reference: '#comment\nfoo: baz \nCloses #1',
-	references: '#comment\nfoo: bar \nCloses #1, #2, #3'
+	references: '#comment\nfoo: bar \nCloses #1, #2, #3',
+	prefix: 'bar REF-1234'
 };
 
 const opts = (async () => {
@@ -24,7 +25,10 @@ const parsed = {
 	reference: (async () =>
 		parse(messages.reference, undefined, (await opts).parserOpts))(),
 	references: (async () =>
-		parse(messages.references, undefined, (await opts).parserOpts))()
+		parse(messages.references, undefined, (await opts).parserOpts))(),
+	prefix: parse(messages.prefix, undefined, {
+		issuePrefixes: ['REF-']
+	})
 };
 
 test('defaults to never and fails for plain', async t => {
@@ -74,3 +78,9 @@ test('fails for references with always', async t => {
 	const expected = false;
 	t.is(actual, expected);
 });
+
+test('succeeds for custom references with always', async t => {
+	const [actual] = referencesEmpty(await parsed.prefix, 'never');
+	const expected = true;
+	t.is(actual, expected);
+});