diff --git a/src/color_ssh/__init__.py b/src/color_ssh/__init__.py
index d18f409..b794fd4 100644
--- a/src/color_ssh/__init__.py
+++ b/src/color_ssh/__init__.py
@@ -1,1 +1,1 @@
-__version__ = '0.0.2'
+__version__ = '0.1.0'
diff --git a/src/color_ssh/color_ssh.py b/src/color_ssh/color_ssh.py
index 4c278c2..20f7651 100644
--- a/src/color_ssh/color_ssh.py
+++ b/src/color_ssh/color_ssh.py
@@ -1,71 +1,135 @@
 from __future__ import division, print_function, absolute_import, unicode_literals
 
 import sys
+import io
 import shlex
 import subprocess
 from optparse import OptionParser
 from color_ssh.util.util import *
+from multiprocessing.pool import Pool
 
 __all__ = []
 
 
 class Setting(object):
     VERSION = 'color-ssh %s' % __import__('color_ssh').__version__
-    USAGE = """%prog [options...] [user@]hostname command"""
+    USAGE = '\n'.join([
+        '%prog [options...] [user@]hostname command',
+        '       %prog [options...] -h host_file command',
+        '       %prog [options...] -H "[user@]hostname [[user@]hostname]...]" command'
+    ])
+    DEFAULT_PARALLELISM = 32
 
-    def __init__(self, label=None, command=None):
-        self.label = label
-        self.command = command
+    def __init__(self, parallelism=None, tasks=None):
+        self.parallelism = parallelism
+        self.tasks = tasks
 
     def parse_args(self, argv, stdout=io2bytes(sys.stdout)):
         """
         :param argv: list of str
         :param stdout: binary-data stdout output
         """
-        parser = OptionParser(version=self.VERSION, usage=self.USAGE)
+        parser = OptionParser(version=self.VERSION, usage=self.USAGE, conflict_handler='resolve')
         parser.allow_interspersed_args = False
 
         parser.add_option(
             '-l', '--label', dest='label', default=None, type='string', metavar='LABEL',
-            help='set label name to LABEL'
+            help='label name'
         )
         parser.add_option(
             '--ssh', dest='ssh', default=str('ssh'), type='string', metavar='SSH',
-            help='override ssh command line string to SSH'
+            help='override ssh command line string'
+        )
+        parser.add_option(
+            '-h', '--hosts', dest='host_file', default=None, type='string', metavar='HOST_FILE',
+            help='hosts file (each line "[user@]host")'
+        )
+        parser.add_option(
+            '-H', '--host', dest='host_string', default=None, type='string', metavar='HOST_STRING',
+            help='additional host entries ("[user@]host")'
+        )
+        parser.add_option(
+            '-p', '--par', dest='parallelism', default=self.DEFAULT_PARALLELISM, type='int', metavar='PAR',
+            help='max number of parallel threads (default: %d)' % self.DEFAULT_PARALLELISM
         )
 
         option, args = parser.parse_args(argv[1:])
+        hosts = self._load_hosts(option.host_file) + (option.host_string.split() if option.host_string else [])
 
-        if len(args) < 2:
+        if len(args) < (1 if hosts else 2):
             stdout.write(arg2bytes(parser.format_help().encode('utf-8')))
             parser.exit(2)
 
-        self.label = option.label or args[0].rsplit('@', 1)[-1]
-        self.command = shlex.split(option.ssh) + args
+        prefix = shlex.split(option.ssh)
+
+        if not hosts:
+            hosts = args[:1]
+            command = args[1:]
+        else:
+            command = args
+
+        tasks = [(option.label or self._extract_label(host), prefix + [host] + command) for host in hosts]
+
+        self.parallelism = option.parallelism
+        self.tasks = tasks
         return self
 
+    @staticmethod
+    def _load_hosts(path):
+        if not path:
+            return []
+
+        with io.open(path) as f:
+            lines = f.readlines()
+        return list(filter(lambda x: x, (line.strip() for line in lines)))
+
+    @staticmethod
+    def _extract_label(host):
+        return host.rsplit('@', 1)[-1]
 
-def main(argv=sys.argv, stdin=io2bytes(sys.stdin), stdout=io2bytes(sys.stdout), stderr=io2bytes(sys.stderr)):
-    """
-    Main function
-    """
-    setting = Setting().parse_args(argv)
-    prefix = ['color-cat', '-l', setting.label]
+
+def run_task(args):
+    label, command = args
+
+    # We don't pass stdout/stderr file descriptors since this function runs in the forked processes.
+    stdout = io2bytes(sys.stdout)
+    stderr = io2bytes(sys.stderr)
+
+    prefix = ['color-cat', '-l', label]
 
     try:
         proc_stdout = subprocess.Popen(prefix, stdin=subprocess.PIPE, stdout=stdout, stderr=stderr)
         proc_stderr = subprocess.Popen(prefix + ['-s', '+'], stdin=subprocess.PIPE, stdout=stderr, stderr=stderr)
-        ret = subprocess.call(setting.command, stdin=stdin, stdout=proc_stdout.stdin, stderr=proc_stderr.stdin)
+        ret = subprocess.call(command, stdin=None, stdout=proc_stdout.stdin, stderr=proc_stderr.stdin)
 
         proc_stdout.stdin.close()
         proc_stderr.stdin.close()
 
         proc_stdout.wait()
         proc_stderr.wait()
+    except Exception as e:
+        msg = '%s: %s\nlabel=%s, command=%s\n' % (e.__class__.__name__, e, label, command)
+        stderr.write(msg.encode('utf-8', 'ignore'))
+        return 1
+    return ret
 
+
+def main(argv=sys.argv, stdout=io2bytes(sys.stdout), stderr=io2bytes(sys.stderr)):
+    """
+    Main function
+    """
+
+    try:
+        setting = Setting().parse_args(argv, stdout)
+        n = min(len(setting.tasks), setting.parallelism)
+        if n <= 1:
+            ret = map(run_task, setting.tasks)
+        else:
+            pool = Pool(n)
+            ret = pool.map(run_task, setting.tasks)
     except Exception as e:
-        msg = '%s: %s\nCommand: %s\n' % (e.__class__.__name__, e, setting.command)
+        msg = '%s: %s\n' % (e.__class__.__name__, e)
         stderr.write(msg.encode('utf-8', 'ignore'))
         return 1
 
-    return ret
+    return max(ret)
diff --git a/src/color_ssh/util/util.py b/src/color_ssh/util/util.py
index 55d27c2..9273954 100644
--- a/src/color_ssh/util/util.py
+++ b/src/color_ssh/util/util.py
@@ -13,4 +13,4 @@ def arg2bytes(arg):
 
 
 def io2bytes(fd):
-    return fd.buffer if PY3 else fd
+    return fd.buffer if hasattr(fd, 'buffer') else fd

Load host list from file
- `--host`
- with parallelism option
    - cf. pssh (parallel-ssh)
diff --git a/tests/color_ssh/test_color_ssh.py b/tests/color_ssh/test_color_ssh.py
index ea82202..b5c4ba7 100644
--- a/tests/color_ssh/test_color_ssh.py
+++ b/tests/color_ssh/test_color_ssh.py
@@ -1,11 +1,11 @@
 # encoding: utf-8
 from __future__ import division, print_function, absolute_import, unicode_literals
 
-import os
-import io
 import sys
+import os
 import tempfile
 import six
+from contextlib import contextmanager
 from mog_commons.unittest import TestCase
 from color_ssh import color_ssh
 from color_ssh.color_ssh import Setting
@@ -13,9 +13,8 @@ from color_ssh.util.util import PY3
 
 
 class TestSetting(TestCase):
-    def _check(self, setting, expected):
-        self.assertEqual(setting.label, expected.label)
-        self.assertEqual(setting.command, expected.command)
+    def _check(self, setting, tasks):
+        self.assertEqual(setting.tasks, tasks)
 
     def _parse(self, args):
         xs = []
@@ -30,35 +29,76 @@ class TestSetting(TestCase):
 
     def test_parse_args(self):
         self._check(self._parse(['server-1', 'pwd']),
-                    Setting('server-1', ['ssh', 'server-1', 'pwd']))
+                    [('server-1', ['ssh', 'server-1', 'pwd'])])
         self._check(self._parse(['user@server-1', 'ls', '-l']),
-                    Setting('server-1', ['ssh', 'user@server-1', 'ls', '-l']))
+                    [('server-1', ['ssh', 'user@server-1', 'ls', '-l'])])
+
+        # label
         self._check(self._parse(['-l', 'label', 'user@server-1', 'ls', '-l']),
-                    Setting('label', ['ssh', 'user@server-1', 'ls', '-l']))
+                    [('label', ['ssh', 'user@server-1', 'ls', '-l'])])
         self._check(self._parse(['--label', 'label', 'user@server-1', 'ls', '-l']),
-                    Setting('label', ['ssh', 'user@server-1', 'ls', '-l']))
+                    [('label', ['ssh', 'user@server-1', 'ls', '-l'])])
         self._check(self._parse(['-llabel', 'user@server-1', 'ls', '-l']),
-                    Setting('label', ['ssh', 'user@server-1', 'ls', '-l']))
+                    [('label', ['ssh', 'user@server-1', 'ls', '-l'])])
         self._check(self._parse(['--label', 'label', '--ssh', '/usr/bin/ssh -v', 'user@server-1', 'ls', '-l']),
-                    Setting('label', ['/usr/bin/ssh', '-v', 'user@server-1', 'ls', '-l']))
+                    [('label', ['/usr/bin/ssh', '-v', 'user@server-1', 'ls', '-l'])])
+
+        # ssh
         self._check(self._parse(['--ssh', '/usr/bin/ssh -v --option "a b c"', 'user@server-1', 'ls', '-l']),
-                    Setting('server-1', ['/usr/bin/ssh', '-v', '--option', 'a b c', 'user@server-1', 'ls', '-l']))
+                    [('server-1', ['/usr/bin/ssh', '-v', '--option', 'a b c', 'user@server-1', 'ls', '-l'])])
         self._check(self._parse(['--label', 'あいう'.encode('utf-8'), 'user@server-1', 'ls', '-l']),
-                    Setting('あいう' if PY3 else 'あいう'.encode('utf-8'), ['ssh', 'user@server-1', 'ls', '-l']))
+                    [('あいう' if PY3 else 'あいう'.encode('utf-8'), ['ssh', 'user@server-1', 'ls', '-l'])])
         self._check(self._parse(['--label', b'\xff\xfe', 'user@server-1', 'ls', '-l']),
-                    Setting('\udcff\udcfe' if PY3 else b'\xff\xfe', ['ssh', 'user@server-1', 'ls', '-l']))
+                    [('\udcff\udcfe' if PY3 else b'\xff\xfe', ['ssh', 'user@server-1', 'ls', '-l'])])
         self._check(self._parse(['server-1', 'echo', b'\xff\xfe']),
-                    Setting('server-1', ['ssh', 'server-1', 'echo', '\udcff\udcfe' if PY3 else b'\xff\xfe']))
+                    [('server-1', ['ssh', 'server-1', 'echo', '\udcff\udcfe' if PY3 else b'\xff\xfe'])])
+
+        # hosts
+        hosts_path = os.path.join('tests', 'resources', 'test_color_ssh_hosts.txt')
+        self._check(self._parse(['-h', hosts_path, 'pwd']), [
+            ('server-1', ['ssh', 'server-1', 'pwd']),
+            ('server-2', ['ssh', 'server-2', 'pwd']),
+            ('server-3', ['ssh', 'server-3', 'pwd']),
+            ('server-4', ['ssh', 'server-4', 'pwd']),
+            ('server-5', ['ssh', 'server-5', 'pwd']),
+            ('server-6', ['ssh', 'server-6', 'pwd']),
+            ('server-7', ['ssh', 'server-7', 'pwd']),
+            ('server-8', ['ssh', 'server-8', 'pwd']),
+            ('server-9', ['ssh', 'root@server-9', 'pwd']),
+            ('server-10', ['ssh', 'root@server-10', 'pwd']),
+        ])
+        self._check(self._parse(['-H', 'server-11 root@server-12', 'pwd']), [
+            ('server-11', ['ssh', 'server-11', 'pwd']),
+            ('server-12', ['ssh', 'root@server-12', 'pwd']),
+        ])
+        self._check(self._parse(['--hosts', hosts_path, '--host', 'server-11 root@server-12', 'pwd']), [
+            ('server-1', ['ssh', 'server-1', 'pwd']),
+            ('server-2', ['ssh', 'server-2', 'pwd']),
+            ('server-3', ['ssh', 'server-3', 'pwd']),
+            ('server-4', ['ssh', 'server-4', 'pwd']),
+            ('server-5', ['ssh', 'server-5', 'pwd']),
+            ('server-6', ['ssh', 'server-6', 'pwd']),
+            ('server-7', ['ssh', 'server-7', 'pwd']),
+            ('server-8', ['ssh', 'server-8', 'pwd']),
+            ('server-9', ['ssh', 'root@server-9', 'pwd']),
+            ('server-10', ['ssh', 'root@server-10', 'pwd']),
+            ('server-11', ['ssh', 'server-11', 'pwd']),
+            ('server-12', ['ssh', 'root@server-12', 'pwd']),
+        ])
+
+        # parallelism
+        self.assertEqual(self._parse(['-H', 'server-11 root@server-12', '-p3', 'pwd']).parallelism, 3)
 
     def test_parse_args_error(self):
         with self.withBytesOutput() as (out, err):
             self.assertSystemExit(2, Setting().parse_args, ['color-ssh'], out)
             self.assertSystemExit(2, Setting().parse_args, ['color-ssh', 'server-1'], out)
             self.assertSystemExit(2, Setting().parse_args, ['color-ssh', '--label', 'x'], out)
+            self.assertSystemExit(2, Setting().parse_args, ['color-ssh', '--host', '  ', 'pwd'], out)
 
 
 class TestMain(TestCase):
-    def test_main(self):
+    def test_main_single_proc(self):
         # requires: POSIX environment, color-cat command
         def f(bs):
             return b'\x1b[7m\x1b[35mtests/resources/test_color_ssh_01.sh\x1b[0m|\x1b[0m\x1b[35m' + bs + b'\n\x1b[0m'
@@ -66,28 +106,83 @@ class TestMain(TestCase):
         def g(bs):
             return b'\x1b[7m\x1b[35mtests/resources/test_color_ssh_01.sh\x1b[0m+\x1b[0m\x1b[35m' + bs + b'\n\x1b[0m'
 
-        with tempfile.TemporaryFile() as out:
-            with tempfile.TemporaryFile() as err:
-                args = ['color-ssh', '--ssh', str('bash'),
-                        os.path.join('tests', 'resources', 'test_color_ssh_01.sh'), 'abc', 'def']
-                ret = color_ssh.main(args, stdout=out, stderr=err)
-                self.assertEqual(ret, 0)
+        with self.__with_temp_output() as (out, err):
+            args = ['color-ssh', '--ssh', str('bash'),
+                    os.path.join('tests', 'resources', 'test_color_ssh_01.sh'), 'abc', 'def']
+            ret = color_ssh.main(args, stdout=out, stderr=err)
+            self.assertEqual(ret, 0)
+
+            out.seek(0)
+            err.seek(0)
+
+            self.assertEqual(out.read(), f(b'abc') + f(b'foo') + f('あいうえお'.encode('utf-8')) + f(b'\xff\xfe'))
+            self.assertEqual(err.read(), g(b'def') + g(b'bar') + g('かきくけこ'.encode('utf-8')) + g(b'\xfd\xfc'))
+
+    def test_main_multi_proc(self):
+        # requires: POSIX environment, color-cat command
+        def f(bs):
+            return b'\x1b[7m\x1b[35mtests/resources/test_color_ssh_01.sh\x1b[0m|\x1b[0m\x1b[35m' + bs + b'\n\x1b[0m'
+
+        def g(bs):
+            return b'\x1b[7m\x1b[35mtests/resources/test_color_ssh_01.sh\x1b[0m+\x1b[0m\x1b[35m' + bs + b'\n\x1b[0m'
+
+        with self.__with_temp_output() as (out, err):
+            path = os.path.join('tests', 'resources', 'test_color_ssh_01.sh')
+            args = ['color-ssh', '--ssh', str('bash'), '-H', '%s %s' % (path, path), 'abc', 'def']
+
+            self.assertFalse(out.closed)
+            self.assertFalse(err.closed)
+
+            ret = color_ssh.main(args, stdout=out, stderr=err)
+            self.assertEqual(ret, 0)
+
+            out.seek(0)
+            err.seek(0)
+
+            self.assertEqual(out.read(), (f(b'abc') + f(b'foo') + f('あいうえお'.encode('utf-8')) + f(b'\xff\xfe')) * 2)
+            self.assertEqual(err.read(), (g(b'def') + g(b'bar') + g('かきくけこ'.encode('utf-8')) + g(b'\xfd\xfc')) * 2)
+
+    def test_main_load_error(self):
+        with self.__with_temp_output() as (out, err):
+            args = ['color-ssh', '-h', 'not_exist_file', '--ssh', str('./tests/resources/not_exist_command'), 'x', 'y']
+            ret = color_ssh.main(args, stdout=out, stderr=err)
+            self.assertEqual(ret, 1)
+
+            out.seek(0)
+            err.seek(0)
+
+            self.assertEqual(out.read(), b'')
+            self.assertTrue(b'No such file or directory' in err.read())
+
+    def test_main_task_error(self):
+        with self.__with_temp_output() as (out, err):
+            args = ['color-ssh', '--ssh', str('./tests/resources/not_exist_command'), 'x', 'y']
+            ret = color_ssh.main(args, stdout=out, stderr=err)
+            self.assertEqual(ret, 1)
 
-                out.seek(0)
-                err.seek(0)
+            out.seek(0)
+            err.seek(0)
 
-                self.assertEqual(out.read(), f(b'abc') + f(b'foo') + f('あいうえお'.encode('utf-8')) + f(b'\xff\xfe'))
-                self.assertEqual(err.read(), g(b'def') + g(b'bar') + g('かきくけこ'.encode('utf-8')) + g(b'\xfd\xfc'))
+            self.assertEqual(out.read(), b'')
+            self.assertTrue(b'No such file or directory' in err.read())
 
-    def test_main_error(self):
+    @staticmethod
+    @contextmanager
+    def __with_temp_output():
         with tempfile.TemporaryFile() as out:
             with tempfile.TemporaryFile() as err:
-                args = ['color-ssh', '--ssh', str('./tests/resources/not_exist_command'), 'x', 'y']
-                ret = color_ssh.main(args, stdout=out, stderr=err)
-                self.assertEqual(ret, 1)
+                old_stdout = sys.stdout
+                old_stderr = sys.stderr
 
-                out.seek(0)
-                err.seek(0)
+                try:
+                    try:
+                        sys.stdout.buffer = out
+                        sys.stderr.buffer = err
+                    except AttributeError:
+                        sys.stdout = out
+                        sys.stderr = err
 
-                self.assertEqual(out.read(), b'')
-                self.assertTrue(b'No such file or directory' in err.read())
+                    yield out, err
+                finally:
+                    sys.stdout = old_stdout
+                    sys.stderr = old_stderr
diff --git a/tests/resources/test_color_ssh_hosts.txt b/tests/resources/test_color_ssh_hosts.txt
new file mode 100644
index 0000000..3b9d2dd
--- /dev/null
+++ b/tests/resources/test_color_ssh_hosts.txt
@@ -0,0 +1,10 @@
+server-1
+server-2
+server-3
+server-4
+server-5
+server-6
+server-7
+server-8
+root@server-9
+root@server-10
\ No newline at end of file

pip install -e .[dev]
parse_log_pytest
pip
six mog-commons pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
-e git+https://github.com/mogproject/color-ssh.git@3c6ef87beb0faf48b0af7f4498b1be5ff34e6fe1#egg=color_ssh
exceptiongroup==1.2.2
iniconfig==2.1.0
Jinja2==3.1.6
MarkupSafe==3.0.2
mog-commons==0.2.3
packaging==24.2
pluggy==1.5.0
pytest==8.3.5
six==1.17.0
tomli==2.2.1

name: color-ssh
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - iniconfig==2.1.0
      - jinja2==3.1.6
      - markupsafe==3.0.2
      - mog-commons==0.2.3
      - packaging==24.2
      - pluggy==1.5.0
      - pytest==8.3.5
      - six==1.17.0
      - tomli==2.2.1
prefix: /opt/conda/envs/color-ssh

tests/color_ssh/test_color_ssh.py::TestSetting::test_parse_args tests/color_ssh/test_color_ssh.py::TestMain::test_main_load_error tests/color_ssh/test_color_ssh.py::TestMain::test_main_multi_proc tests/color_ssh/test_color_ssh.py::TestMain::test_main_single_proc tests/color_ssh/test_color_ssh.py::TestMain::test_main_task_error
tests/color_ssh/test_color_ssh.py::TestSetting::test_parse_args_error
src/color_ssh/util/util.py src/color_ssh/color_ssh.py src/color_ssh/__init__.py
src/color_ssh/util/util.py src/color_ssh/color_ssh.py src/color_ssh/__init__.py