diff --git a/packages/cli/.eslintignore b/packages/cli/.eslintignore
new file mode 100644
index 0000000000..a65b41774a
--- /dev/null
+++ b/packages/cli/.eslintignore
@@ -0,0 +1 @@
+lib
diff --git a/packages/cli/.eslintrc b/packages/cli/.eslintrc
new file mode 100644
index 0000000000..68c6a917b5
--- /dev/null
+++ b/packages/cli/.eslintrc
@@ -0,0 +1,8 @@
+{
+  "extends": [
+    "oclif",
+    "oclif-typescript"
+  ],
+  "rules": {
+  }
+}
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 71f0905008..0892196f30 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -59,7 +59,6 @@
   "devDependencies": {
     "@oclif/dev-cli": "^1.21.3",
     "@oclif/test": "^1.2.4",
-    "@oclif/tslint": "^3.1.1",
     "@types/ansi-styles": "^3.2.1",
     "@types/chai": "^4.1.7",
     "@types/debug": "^4.1.2",
@@ -74,6 +73,9 @@
     "@types/write-json-file": "^2.2.1",
     "aws-sdk": "^2.421.0",
     "chai": "^4.2.0",
+    "eslint": "^6.7.2",
+    "eslint-config-oclif": "^3.1.0",
+    "eslint-config-oclif-typescript": "^0.1.0",
     "globby": "^10.0.1",
     "lerna": "^3.18.0",
     "lodash": "^4.17.11",
@@ -83,7 +85,6 @@
     "read-pkg": "^4.0.1",
     "sinon": "^7.2.4",
     "ts-node": "^8.0.2",
-    "tslint": "^5.11.0",
     "typescript": "3.3.3333"
   },
   "engines": {
@@ -274,11 +275,13 @@
   },
   "repository": "heroku/cli",
   "scripts": {
+    "lint": "eslint . --ext .ts --config .eslintrc",
     "build": "rm -rf lib && tsc",
     "postpublish": "rm -f oclif.manifest.json",
-    "posttest": "tsc -p test --noEmit && tslint -p test -t stylish",
     "prepack": "yarn run build && oclif-dev manifest",
+    "pretest": "tsc -p test --noEmit",
     "test": "mocha --forbid-only \"test/**/*.test.ts\"",
+    "posttest": "yarn lint",
     "version": "oclif-dev readme --multi && git add README.md ../../docs"
   },
   "types": "lib/index.d.ts"
diff --git a/packages/cli/src/analytics.ts b/packages/cli/src/analytics.ts
index ac65791b9e..8019de47ae 100644
--- a/packages/cli/src/analytics.ts
+++ b/packages/cli/src/analytics.ts
@@ -8,31 +8,33 @@ import deps from './deps'
 const debug = require('debug')('heroku:analytics')
 
 export interface RecordOpts {
-  Command: Config.Command.Class
-  argv: string[]
+  Command: Config.Command.Class;
+  argv: string[];
 }
 
 export interface AnalyticsInterface {
-  source: string,
-  event: string,
+  source: string;
+  event: string;
   properties: {
-    cli: string,
-    command: string,
-    completion: number,
-    version: string,
-    plugin: string,
-    plugin_version: string,
-    os: string,
-    shell: string,
-    valid: boolean,
-    language: string,
-    install_id: string,
-  }
+    cli: string;
+    command: string;
+    completion: number;
+    version: string;
+    plugin: string;
+    plugin_version: string;
+    os: string;
+    shell: string;
+    valid: boolean;
+    language: string;
+    install_id: string;
+  };
 }
 
 export default class AnalyticsCommand {
   config: Config.IConfig
+
   userConfig!: typeof deps.UserConfig.prototype
+
   http: typeof deps.HTTP
 
   constructor(config: Config.IConfig) {
@@ -67,15 +69,14 @@ export default class AnalyticsCommand {
         valid: true,
         language: 'node',
         install_id: this.userConfig.install,
-      }
+      },
     }
 
     const data = Buffer.from(JSON.stringify(analyticsData)).toString('base64')
     if (this.authorizationToken) {
       return this.http.get(`${this.url}?data=${data}`, {headers: {authorization: `Bearer ${this.authorizationToken}`}}).catch(error => debug(error))
-    } else {
-      return this.http.get(`${this.url}?data=${data}`).catch(error => debug(error))
     }
+    return this.http.get(`${this.url}?data=${data}`).catch(error => debug(error))
   }
 
   get url(): string {
@@ -92,7 +93,7 @@ export default class AnalyticsCommand {
 
   get usingHerokuAPIKey(): boolean {
     const k = process.env.HEROKU_API_KEY
-    return !!(k && k.length > 0)
+    return Boolean(k && k.length > 0)
   }
 
   get netrcLogin(): string | undefined {
@@ -106,8 +107,8 @@ export default class AnalyticsCommand {
 
   async _acAnalytics(id: string): Promise<number> {
     if (id === 'autocomplete:options') return 0
-    let root = path.join(this.config.cacheDir, 'autocomplete', 'completion_analytics')
-    let meta = {
+    const root = path.join(this.config.cacheDir, 'autocomplete', 'completion_analytics')
+    const meta = {
       cmd: deps.file.exists(path.join(root, 'command')),
       flag: deps.file.exists(path.join(root, 'flag')),
       value: deps.file.exists(path.join(root, 'value')),
diff --git a/packages/cli/src/deps.ts b/packages/cli/src/deps.ts
index 2d9e75a025..ca9d94c043 100644
--- a/packages/cli/src/deps.ts
+++ b/packages/cli/src/deps.ts
@@ -1,8 +1,16 @@
-import FS = require('fs-extra')
 import {HTTP} from 'http-call'
+import UserConfig from './user-config'
+import FS = require('fs-extra')
 
 import file = require('./file')
-import UserConfig from './user-config'
+
+const cache: any = {}
+function fetch(s: string) {
+  if (!cache[s]) {
+    cache[s] = require(s)
+  }
+  return cache[s]
+}
 
 export default {
   get fs(): typeof FS {
@@ -18,12 +26,3 @@ export default {
     return fetch('./user-config').default
   },
 }
-
-const cache: any = {}
-
-function fetch(s: string) {
-  if (!cache[s]) {
-    cache[s] = require(s)
-  }
-  return cache[s]
-}
diff --git a/packages/cli/src/file.ts b/packages/cli/src/file.ts
index 921f31517a..07545ed97c 100644
--- a/packages/cli/src/file.ts
+++ b/packages/cli/src/file.ts
@@ -6,8 +6,6 @@ import deps from './deps'
 const debug = require('debug')('heroku-cli:file')
 
 export function exists(f: string): Promise<boolean> {
-  // debug('exists', f)
-  // @ts-ignore
   return deps.fs.pathExists(f)
 }
 
@@ -28,8 +26,8 @@ export async function remove(file: string) {
 }
 
 export async function ls(dir: string): Promise<{ path: string; stat: FS.Stats }[]> {
-  let files = await deps.fs.readdir(dir)
-  let paths = files.map(f => path.join(dir, f))
+  const files = await deps.fs.readdir(dir)
+  const paths = files.map(f => path.join(dir, f))
   return Promise.all(paths.map(path => deps.fs.stat(path).then(stat => ({path, stat}))))
 }
 
@@ -37,14 +35,15 @@ export async function removeEmptyDirs(dir: string): Promise<void> {
   let files
   try {
     files = await ls(dir)
-  } catch (err) {
-    if (err.code === 'ENOENT') return
-    throw err
+  } catch (error) {
+    if (error.code === 'ENOENT') return
+    throw error
   }
-  let dirs = files.filter(f => f.stat.isDirectory()).map(f => f.path)
-  for (let p of dirs.map(removeEmptyDirs)) await p
+  const dirs = files.filter(f => f.stat.isDirectory()).map(f => f.path)
+  // eslint-disable-next-line no-await-in-loop
+  for (const p of dirs.map(removeEmptyDirs)) await p
   files = await ls(dir)
-  if (!files.length) await remove(dir)
+  if (files.length === 0) await remove(dir)
 }
 
 export async function readJSON(file: string) {
diff --git a/packages/cli/src/global.d.ts b/packages/cli/src/global.d.ts
index b5b4e7eed4..5cecf5b3bd 100644
--- a/packages/cli/src/global.d.ts
+++ b/packages/cli/src/global.d.ts
@@ -2,7 +2,7 @@
 
 declare namespace NodeJS {
   interface Global {
-    columns?: number
-    testing?: boolean
+    columns?: number;
+    testing?: boolean;
   }
 }
diff --git a/packages/cli/src/hooks/init/version.ts b/packages/cli/src/hooks/init/version.ts
index b4286ce9cd..f3d9df9e0d 100644
--- a/packages/cli/src/hooks/init/version.ts
+++ b/packages/cli/src/hooks/init/version.ts
@@ -13,9 +13,9 @@ const Whitelist = [
 
 export const version: Hook.Init = async function () {
   if (['-v', '--version', 'version'].includes(process.argv[2])) {
-    for (let env of Whitelist) {
+    for (const env of Whitelist) {
       if (process.env[env]) {
-        let value = env === 'HEROKU_API_KEY' ? 'to [REDACTED]' : `to ${process.env[env]}`
+        const value = env === 'HEROKU_API_KEY' ? 'to [REDACTED]' : `to ${process.env[env]}`
         this.warn(`${env} set ${value}`)
       }
     }
diff --git a/packages/cli/src/hooks/update/b.ts b/packages/cli/src/hooks/update/b.ts
index 7931e54625..3ab8968d30 100644
--- a/packages/cli/src/hooks/update/b.ts
+++ b/packages/cli/src/hooks/update/b.ts
@@ -13,8 +13,8 @@ function brew(args: string[], opts: SpawnSyncOptions = {}) {
 
 interface InstallReceipt {
   source: {
-    tap: string
-  }
+    tap: string;
+  };
 }
 
 export const brewHook: Hook<'update'> = async function () {
@@ -24,9 +24,9 @@ export const brewHook: Hook<'update'> = async function () {
   let binPath
   try {
     binPath = fs.realpathSync(path.join(brewRoot, 'bin/heroku'))
-  } catch (err) {
-    if (err.code === 'ENOENT') return
-    throw err
+  } catch (error) {
+    if (error.code === 'ENOENT') return
+    throw error
   }
   let cellarPath: string
   if (binPath && binPath.startsWith(path.join(brewRoot, 'Cellar'))) {
@@ -39,7 +39,7 @@ export const brewHook: Hook<'update'> = async function () {
   }
 
   const needsMigrate = async (): Promise<boolean> => {
-    let receipt = await fetchInstallReceipt()
+    const receipt = await fetchInstallReceipt()
     if (!receipt) return false
     return receipt.source.tap === 'homebrew/core'
   }
diff --git a/packages/cli/src/hooks/update/brew.ts b/packages/cli/src/hooks/update/brew.ts
index 7931e54625..3ab8968d30 100644
--- a/packages/cli/src/hooks/update/brew.ts
+++ b/packages/cli/src/hooks/update/brew.ts
@@ -13,8 +13,8 @@ function brew(args: string[], opts: SpawnSyncOptions = {}) {
 
 interface InstallReceipt {
   source: {
-    tap: string
-  }
+    tap: string;
+  };
 }
 
 export const brewHook: Hook<'update'> = async function () {
@@ -24,9 +24,9 @@ export const brewHook: Hook<'update'> = async function () {
   let binPath
   try {
     binPath = fs.realpathSync(path.join(brewRoot, 'bin/heroku'))
-  } catch (err) {
-    if (err.code === 'ENOENT') return
-    throw err
+  } catch (error) {
+    if (error.code === 'ENOENT') return
+    throw error
   }
   let cellarPath: string
   if (binPath && binPath.startsWith(path.join(brewRoot, 'Cellar'))) {
@@ -39,7 +39,7 @@ export const brewHook: Hook<'update'> = async function () {
   }
 
   const needsMigrate = async (): Promise<boolean> => {
-    let receipt = await fetchInstallReceipt()
+    const receipt = await fetchInstallReceipt()
     if (!receipt) return false
     return receipt.source.tap === 'homebrew/core'
   }
diff --git a/packages/cli/src/hooks/update/plugin-migrate.ts b/packages/cli/src/hooks/update/plugin-migrate.ts
index 261c574792..3eafbfa7a4 100644
--- a/packages/cli/src/hooks/update/plugin-migrate.ts
+++ b/packages/cli/src/hooks/update/plugin-migrate.ts
@@ -28,25 +28,27 @@ export const migrate: Hook<'init'> = async function () {
       const p = path.join(pluginsDir, 'user.json')
       if (await fs.pathExists(p)) {
         const {manifest} = await fs.readJSON(p)
-        for (let plugin of Object.keys(manifest.plugins)) {
+        for (const plugin of Object.keys(manifest.plugins)) {
           process.stderr.write(`heroku-cli: migrating ${plugin}\n`)
+          // eslint-disable-next-line no-await-in-loop
           await exec('heroku', ['plugins:install', plugin])
         }
       }
-    } catch (err) {
-      this.warn(err)
+    } catch (error) {
+      this.warn(error)
     }
     try {
       const p = path.join(pluginsDir, 'link.json')
       if (await fs.pathExists(p)) {
         const {manifest} = await fs.readJSON(path.join(pluginsDir, 'link.json'))
-        for (let {root} of Object.values(manifest.plugins) as any) {
+        for (const {root} of Object.values(manifest.plugins) as any) {
           process.stderr.write(`heroku-cli: migrating ${root}\n`)
+          // eslint-disable-next-line no-await-in-loop
           await exec('heroku', ['plugins:link', root])
         }
       }
-    } catch (err) {
-      this.warn(err)
+    } catch (error) {
+      this.warn(error)
     }
     await fs.remove(pluginsDir)
     process.stderr.write('heroku: done migrating plugins\n')
diff --git a/packages/cli/src/hooks/update/tidy.ts b/packages/cli/src/hooks/update/tidy.ts
index c9a32e0076..d97bbe46f9 100644
--- a/packages/cli/src/hooks/update/tidy.ts
+++ b/packages/cli/src/hooks/update/tidy.ts
@@ -5,13 +5,13 @@ import deps from '../../deps'
 
 export const tidy: Hook<'update'> = async function () {
   const cleanupPlugins = async () => {
-    let pluginsDir = path.join(this.config.dataDir, 'plugins')
+    const pluginsDir = path.join(this.config.dataDir, 'plugins')
     if (await deps.file.exists(path.join(pluginsDir, 'plugins.json'))) return
     let pjson
     try {
       pjson = await deps.file.readJSON(path.join(pluginsDir, 'package.json'))
-    } catch (err) {
-      if (err.code !== 'ENOENT') throw err
+    } catch (error) {
+      if (error.code !== 'ENOENT') throw error
       return
     }
     if (!pjson.dependencies || pjson.dependencies === {}) {
diff --git a/packages/cli/src/user-config.ts b/packages/cli/src/user-config.ts
index d9818dd41b..1e1f48bd75 100644
--- a/packages/cli/src/user-config.ts
+++ b/packages/cli/src/user-config.ts
@@ -4,27 +4,34 @@ import * as path from 'path'
 import deps from './deps'
 
 export interface ConfigJSON {
-  schema: 1
-  install?: string
-  skipAnalytics?: boolean
+  schema: 1;
+  install?: string;
+  skipAnalytics?: boolean;
 }
 
 export default class UserConfig {
   private needsSave = false
+
   private body!: ConfigJSON
+
   private mtime?: number
+
   private saving?: Promise<void>
+
   private _init!: Promise<void>
 
+  // eslint-disable-next-line no-useless-constructor
   constructor(private readonly config: Config.IConfig) {}
 
   public get install() {
     return this.body.install || this.genInstall()
   }
+
   public set install(install: string) {
     this.body.install = install
     this.needsSave = true
   }
+
   public get skipAnalytics() {
     if (this.config.scopedEnvVar('SKIP_ANALYTICS') === '1') return true
     if (typeof this.body.skipAnalytics !== 'boolean') {
@@ -37,7 +44,8 @@ export default class UserConfig {
   public async init() {
     await this.saving
     if (this._init) return this._init
-    return (this._init = (async () => {
+
+    this._init = (async () => {
       this.debug('init')
       this.body = (await this.read()) || {schema: 1}
 
@@ -51,7 +59,9 @@ export default class UserConfig {
       this.skipAnalytics
 
       if (this.needsSave) await this.save()
-    })())
+    })()
+
+    return this._init
   }
 
   private get debug() {
@@ -78,17 +88,17 @@ export default class UserConfig {
     await this.migrate()
     try {
       this.mtime = await this.getLastUpdated()
-      let body = await deps.file.readJSON(this.file)
+      const body = await deps.file.readJSON(this.file)
       return body
-    } catch (err) {
-      if (err.code !== 'ENOENT') throw err
+    } catch (error) {
+      if (error.code !== 'ENOENT') throw error
       this.debug('not found')
     }
   }
 
   private async migrate() {
     if (await deps.file.exists(this.file)) return
-    let old = path.join(this.config.configDir, 'config.json')
+    const old = path.join(this.config.configDir, 'config.json')
     if (!await deps.file.exists(old)) return
     this.debug('moving config into new place')
     await deps.file.rename(old, this.file)
@@ -103,8 +113,8 @@ export default class UserConfig {
     try {
       const stat = await deps.file.stat(this.file)
       return stat.mtime.getTime()
-    } catch (err) {
-      if (err.code !== 'ENOENT') throw err
+    } catch (error) {
+      if (error.code !== 'ENOENT') throw error
     }
   }
 
diff --git a/packages/cli/test/acceptance/plugin.test.ts b/packages/cli/test/acceptance/plugin.test.ts
index 36dc837f6b..6e50838052 100755
--- a/packages/cli/test/acceptance/plugin.test.ts
+++ b/packages/cli/test/acceptance/plugin.test.ts
@@ -11,7 +11,7 @@ const plugins = ['heroku-ps-exec']
 const skipOnWindows = process.platform === 'win32' ? it.skip : it
 
 describe.skip('plugins', () => {
-  plugins.map(plugin => {
+  plugins.forEach(plugin => {
     skipOnWindows(plugin, async () => {
       const cwd = path.join(__dirname, '../../tmp/plugin', plugin)
       await fs.remove(cwd)
diff --git a/packages/cli/test/acceptance/smoke.test.ts b/packages/cli/test/acceptance/smoke.test.ts
index 08ef696ffe..97c690ef78 100755
--- a/packages/cli/test/acceptance/smoke.test.ts
+++ b/packages/cli/test/acceptance/smoke.test.ts
@@ -25,7 +25,7 @@ describe('smoke', () => {
   })
 
   it('heroku apps', async () => {
-    let cmd = await run('apps')
+    const cmd = await run('apps')
     expect(cmd.stdout).to.match(/^===.*Apps/)
   })
 
@@ -43,7 +43,7 @@ describe('smoke', () => {
   })
 
   it('asserts oclif plugins are in core', async () => {
-    let cmd = await run('plugins --core')
+    const cmd = await run('plugins --core')
     expect(cmd.stdout).to.contain('@oclif/plugin-commands')
     expect(cmd.stdout).to.contain('@oclif/plugin-help')
     expect(cmd.stdout).to.contain('@oclif/plugin-legacy')
@@ -56,11 +56,11 @@ describe('smoke', () => {
 
   it('asserts monorepo plugins are in core', async () => {
     let paths = await globby(['packages/*/package.json'])
-    let cmd = await run('plugins --core')
+    const cmd = await run('plugins --core')
     paths = paths.map((p: string) => p.replace('packages/', '').replace('/package.json', ''))
     console.log(paths)
     paths = paths.filter((p: string) => p === 'cli')
-    paths.map((plugin: string) => {
+    paths.forEach((plugin: string) => {
       expect(cmd.stdout).to.contain(plugin)
     })
   })
diff --git a/packages/cli/test/analytics.test.ts b/packages/cli/test/analytics.test.ts
index bb2775d4d2..5e872d5ad2 100644
--- a/packages/cli/test/analytics.test.ts
+++ b/packages/cli/test/analytics.test.ts
@@ -8,19 +8,19 @@ import AnalyticsCommand, {AnalyticsInterface} from '../src/analytics'
 import UserConfig from '../src/user-config'
 
 function createBackboardMock(expectedGetter: (data: AnalyticsInterface) => any, actual: any) {
-  let backboard = nock('https://backboard.heroku.com/', {
+  const backboard = nock('https://backboard.heroku.com/', {
     reqheaders: {
       'user-agent': '@oclif/command/1.5.6 darwin-x64 node-v10.2.1',
-    }
+    },
   })
-    .get('/hamurai')
-    .query(({data: analyticsData}: { data: string }) => {
-      const data: AnalyticsInterface = JSON.parse(Buffer.from(analyticsData, 'base64').toString())
-      const expected = expectedGetter(data)
-      expect(expected).to.eq(actual)
-      return true
-    })
-    .reply(200)
+  .get('/hamurai')
+  .query(({data: analyticsData}: { data: string }) => {
+    const data: AnalyticsInterface = JSON.parse(Buffer.from(analyticsData, 'base64').toString())
+    const expected = expectedGetter(data)
+    expect(expected).to.eq(actual)
+    return true
+  })
+  .reply(200)
 
   return backboard
 }
@@ -36,9 +36,9 @@ async function runAnalyticsTest(expectedCbk: (data: AnalyticsInterface) => any,
   Login.plugin = {name: 'foo', version: '123'} as any
   Login.id = 'login'
 
-  let backboard = createBackboardMock(expectedCbk, actual)
+  const backboard = createBackboardMock(expectedCbk, actual)
   await analytics.record({
-    Command: Login, argv: ['foo', 'bar']
+    Command: Login, argv: ['foo', 'bar'],
   })
   backboard.done()
 }
@@ -52,10 +52,10 @@ describe('analytics (backboard has an error)', () => {
   })
 
   it('does not show an error on console', async () => {
-    let backboard = nock('https://backboard.heroku.com/')
-      .get('/hamurai')
-      .query(() => true)
-      .reply(500)
+    const backboard = nock('https://backboard.heroku.com/')
+    .get('/hamurai')
+    .query(() => true)
+    .reply(500)
 
     const config = await Config.load()
     config.platform = 'win32'
@@ -69,7 +69,7 @@ describe('analytics (backboard has an error)', () => {
 
     try {
       await analytics.record({
-        Command: Login, argv: ['foo', 'bar']
+        Command: Login, argv: ['foo', 'bar'],
       })
     } catch {
       throw new Error('Expected analytics hook to 🦃 error')
diff --git a/packages/cli/tslint.json b/packages/cli/tslint.json
deleted file mode 100644
index f5703540c9..0000000000
--- a/packages/cli/tslint.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-  "extends": "@oclif/tslint"
-}