diff --git a/setup.cfg b/setup.cfg
index 9be17a2..acdc974 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -46,6 +46,7 @@ scrypt =
 [options.entry_points]
 console_scripts = 
 	passlib-mkpasswd = passlib_cli.__main__:main
+	passlib-totp = passlib_cli.totp:main
 	passlib-autocomplete = passlib_cli.complete:main
 
 [aliases]
diff --git a/src/passlib_cli/totp.py b/src/passlib_cli/totp.py
new file mode 100644
index 0000000..294a888
--- /dev/null
+++ b/src/passlib_cli/totp.py
@@ -0,0 +1,97 @@
+#!/usr/bin/env python
+# encoding: utf-8
+""" TOTP CLI utils. """
+
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+    unicode_literals
+)
+
+import argparse
+import logging
+import string
+import sys
+import textwrap
+import time
+
+from passlib import totp
+
+from . import cli_utils
+
+logger = logging.getLogger(__name__)
+
+BASE32_HINT = set(string.ascii_uppercase + '234567')
+
+
+def get_totp(secret):
+    if secret.startswith('otpauth://'):
+        return totp.TOTP.from_uri(secret)
+    is_base32_like = not (set(secret) - BASE32_HINT)
+    try:
+        int(secret, 16)
+        is_hex_like = True
+    except ValueError:
+        is_hex_like = False
+
+    if is_base32_like:
+        return totp.TOTP(key=secret, format='base32')
+    elif is_hex_like:
+        return totp.TOTP(key=secret, format='hex')
+
+    raise ValueError('invalid secret')
+
+
+def get_token(t):
+    return t.generate()
+
+
+parser = argparse.ArgumentParser(
+    description="Generate TOTP codes using passlib",
+)
+parser.add_argument(
+    '--live',
+    action='store_true',
+    help=textwrap.dedent(
+        """
+        Keep generating one-time passwords
+        """
+    ),
+)
+cli_utils.add_version_arg(parser)
+cli_utils.add_verbosity_mutex(parser)
+
+
+def main(inargs=None):
+    # print(__name__)
+    # print(__package__)
+    # print(__file__)
+    # print(os.path.basename(__file__))
+    args = parser.parse_args(inargs)
+    cli_utils.setup_logging(args.verbosity)
+
+    secret = sys.stdin.readline().rstrip()
+    generator = get_totp(secret)
+
+    def needs_wait(token):
+        return token.expire_time - time.time()
+
+    while True:
+        token = get_token(generator)
+        print(token.token)
+
+        if args.live:
+            while (tleft := needs_wait(token)) > 0:
+                logger.debug("time left: %d", tleft)
+                # sleep for one second at a time to catch e.g.
+                # KeyboardInterrupts
+                time.sleep(1)
+            continue
+        else:
+            break
+
+
+if __name__ == '__main__':
+    parser.prog = 'python -m ' + __spec__.name
+    main()