diff --git a/README.rst b/README.rst
index b95f329..0c24344 100644
--- a/README.rst
+++ b/README.rst
@@ -36,8 +36,16 @@ One way to use this is:
 
 .. code:: sh
 
-   $ sudo $(which tasker) create <IMAGE_URL> "<COMMANDS>"
-   8935 # This is the PID of the new process.
+   # Root permissions are necessary to create the task.
+   $ sudo $(which tasker) create <IMAGE_URL> "sleep 100"
+   8935 # This is the ID of the new task.
+   $ tasker health_check 8935
+   exists: True
+   status: sleeping
+   $ sudo $(which tasker) send_signal SIGINT
+   $ tasker health_check 8935
+   exists: False
+   status: None
 
 ``tasker`` downloads the image from the given ``<IMAGE_URL>`` into the current working directory.
 Also in the directory, the image is untarred to create a "filesystem".
@@ -49,38 +57,42 @@ Library
 
 ``tasker`` is a Python library.
 
-To install ``tasker``:
+Installation
+^^^^^^^^^^^^
 
 .. code:: sh
 
    pip install -e .
 
+API
+^^^
+
 To use ``tasker``:
 
 .. code:: python
 
    import os
    import pathlib
+   import signal
 
    from tasker.tasker import Task
 
-   # An image to download, extract and create a chroot jail in.
-   image_url = 'http://example.com/image.tar'
-
-   # The image will be downloaded and extracted into the download_path.
-   download_path = pathlib.Path(os.getcwd())
-
-   # See ``args`` at
-   # https://docs.python.org/2/library/subprocess.html#subprocess.Popen
-   args = ['echo', '1']
-
    task = Task(
-      image_url=image_url,
-      args=args,
-      download_path=download_path,
+      # An image to download, extract and create a chroot jail in.
+      image_url='http://example.com/image.tar',
+      # A command to run in the extracted filesystem.
+      args=['top'],
+      # Where the image will be downloaded and extracted into.
+      download_path=pathlib.Path(os.getcwd()),
    )
 
-   pid = task.process.pid
+   task_health = task.get_health()
+   # {"running": True, "time": "0:46"}
+
+   task.send_signal(signal.SIGTERM)
+
+   task_health = task.get_health()
+   # {"running": False, "time": "0:46"}
 
 Supported platforms
 -------------------
@@ -108,7 +120,7 @@ In the Vagrant box, create a ``virtualenv``:
 
 .. code:: sh
 
-   mkvirtualenv -p python3.5 chroot_tasker
+   mkvirtualenv -p python3.5 tasker
 
 Install the test dependencies:
 
@@ -152,4 +164,12 @@ There are at least three options for the directory in which to create the filesy
 
 The current implementation is (2).
 Ideally there would be multiple of the above, with (2) as the default.
-The issue for this is https://github.com/adamtheturtle/chroot_tasker/issues/24.
\ No newline at end of file
+The issue for this is https://github.com/adamtheturtle/chroot_tasker/issues/24.
+
+Identifiers
+^^^^^^^^^^^
+
+This uses PIDs as identifiers.
+This is not safe - PIDs get reused and so this could end up with a user manipulating the wrong process.
+This was a simple to implement strategy.
+A long term solution might be stateful and have a mapping of tasks to unique identifiers.
diff --git a/cli/cli.py b/cli/cli.py
index 5b4afaa..f5f414e 100644
--- a/cli/cli.py
+++ b/cli/cli.py
@@ -5,6 +5,7 @@ CLI for creating and interacting with tasks.
 import os
 import pathlib
 import shlex
+from signal import Signals
 
 import click
 
@@ -35,4 +36,33 @@ def create(image_url, args):
         args=shlex.split(args),
         download_path=pathlib.Path(os.getcwd()),
     )
-    print(task.process.pid)
+
+    print(task.id)
+
+
+@cli.command('health_check')
+@click.argument('task_id')
+def health_check(task_id):
+    """
+    Check the health of a task.
+
+    :param str task_id: The id of an existing task.
+    """
+    task = Task(existing_task=int(task_id))
+    health = task.get_health()
+    print('exists: ' + str(health['exists']))
+    print('status: ' + str(health['status']))
+
+
+@cli.command('send_signal')
+@click.argument('task_id')
+@click.argument('signal')
+def send_signal(task_id, signal):
+    """
+    Send a signal to a process started by an existing task.
+
+    :param str task_id: The id of an existing task.
+    :param str signal: The signal to send.
+    """
+    task = Task(existing_task=int(task_id))
+    task.send_signal(Signals[signal])
diff --git a/tasker/tasker.py b/tasker/tasker.py
index b1e86c9..f475971 100644
--- a/tasker/tasker.py
+++ b/tasker/tasker.py
@@ -3,8 +3,10 @@ Create and interact with tasks in a chroot jail.
 """
 
 import os
+import psutil
 import subprocess
 import tarfile
+import time
 import urllib.parse
 import urllib.request
 import uuid
@@ -62,6 +64,12 @@ def _run_chroot_process(filesystem, args):
     os.fchdir(real_root)
     os.chroot(".")
     os.close(real_root)
+
+    # On some platforms it seems to take some time for a process to start,
+    # even after this point. Therefore sleep for 5ms to ensure platform
+    # parity. This seems to be necessary on Travis CI hosted.
+    time.sleep(0.05)
+
     return process
 
 
@@ -70,7 +78,36 @@ class Task(object):
     A process in a chroot jail.
     """
 
-    def __init__(self, image_url, args, download_path):
+    def get_health(self):
+        """
+        Get details of the task's health.
+
+        :rtype: dict
+        :returns: The task's process's status.
+        """
+        if self._process is not None:
+            try:
+                status = self._process.status()
+                return {'exists': True, 'status': status}
+            except psutil.NoSuchProcess:
+                pass
+
+        return {'exists': False, 'status': None}
+
+    def send_signal(self, signal):
+        """
+        Send a signal to the task's process.
+
+        :param int signal: The signal to send.
+        """
+        self._process.send_signal(signal)
+        try:
+            os.wait()
+        except OSError:  # pragma: no cover
+            pass
+
+    def __init__(self, image_url=None, args=None, download_path=None,
+                 existing_task=None):
         """
         Create a new task, which is a process running inside a chroot with root
         being a downloaded image's root.
@@ -78,13 +115,29 @@ class Task(object):
         :param str image_url: The url of a ``.tar`` file.
         :param list args: List of strings. See ``subprocess.Popen.args``.
         :param pathlib.Path download_path: The parent to extract the downloaded
-            image into.
+        image into.
+        :param existing_task: The id of an existing task. If this is given,
+            other parameters are ignored and no new process is started.
+
+        :ivar int id: An identifier for the task.
         """
-        filesystem = _create_filesystem_dir(
-            image_url=image_url,
-            download_path=download_path,
-        )
-        self.process = _run_chroot_process(
-            filesystem=filesystem,
-            args=args,
-        )
+        if existing_task is not None:
+            try:
+                self._process = psutil.Process(existing_task)
+            except psutil.NoSuchProcess:
+                self._process = None
+
+            self.id = existing_task
+        else:
+            filesystem = _create_filesystem_dir(
+                image_url=image_url,
+                download_path=download_path,
+            )
+
+            process = _run_chroot_process(
+                filesystem=filesystem,
+                args=args,
+            )
+
+            self.id = process.pid
+            self._process = psutil.Process(self.id)

Library function for sending signals
Also think how this will be presented as part of the CLI.
diff --git a/cli/tests/test_cli.py b/cli/tests/test_cli.py
index c84b10c..dea2867 100644
--- a/cli/tests/test_cli.py
+++ b/cli/tests/test_cli.py
@@ -4,12 +4,11 @@ Tests for ``cli.cli``.
 
 import os
 
-import psutil
-
 from click.testing import CliRunner
 
 from cli.cli import cli
 from common.testtools import ROOTFS_URI
+from tasker.tasker import Task
 
 
 class TestCreate(object):
@@ -26,12 +25,27 @@ class TestCreate(object):
         os.chdir(tmpdir.strpath)
 
         runner = CliRunner()
-        subcommand = 'create'
-        commands = 'sleep 10'
-        result = runner.invoke(cli, [subcommand, ROOTFS_URI, commands])
-        process = psutil.Process(int(result.output))
-        cmdline = process.cmdline()
-        process.kill()
-
+        commands = 'sleep 5'
+        result = runner.invoke(cli, ['create', ROOTFS_URI, commands])
         assert result.exit_code == 0
-        assert cmdline == commands.split()
+        task = Task(existing_task=int(result.output))
+        assert task._process.cmdline() == commands.split()
+
+    def test_send_signal_healthcheck(self, tmpdir):
+        """
+        Sending a SIGTERM signal to a task stops the process running.
+        The status before and after is relayed by the healthcheck function.
+        """
+        # Change directory to temporary directory so as not to pollute current
+        # working directory with downloaded filesystem.
+        os.chdir(tmpdir.strpath)
+
+        runner = CliRunner()
+        create = runner.invoke(cli, ['create', ROOTFS_URI, 'sleep 5'])
+        task_id = create.output
+
+        before_int = runner.invoke(cli, ['health_check', task_id])
+        assert before_int.output == 'exists: True\nstatus: sleeping\n'
+        runner.invoke(cli, ['send_signal', task_id, 'SIGINT'])
+        after_int = runner.invoke(cli, ['health_check', task_id])
+        assert after_int.output == 'exists: False\nstatus: None\n'
diff --git a/tasker/tests/test_tasker.py b/tasker/tests/test_tasker.py
index 77eb1ae..a491625 100644
--- a/tasker/tests/test_tasker.py
+++ b/tasker/tests/test_tasker.py
@@ -2,8 +2,8 @@
 Tests for ``tasker.tasker``.
 """
 
+import signal
 import tarfile
-import time
 
 import pathlib
 import psutil
@@ -106,8 +106,6 @@ class TestRunChrootProcess(object):
             args=['touch', '/example.txt'],
         )
 
-        # ``touch`` takes a short time to work.
-        time.sleep(0.01)
         assert filesystem.joinpath('example.txt').exists()
 
     def test_process_returned(self, tmpdir):
@@ -156,7 +154,36 @@ class TestTask(object):
         """
         task = Task(
             image_url=ROOTFS_URI,
-            args=['echo', '1'],
+            args=['sleep', '5'],
+            download_path=pathlib.Path(tmpdir.strpath),
+        )
+
+        assert task.get_health() == {'exists': True, 'status': 'sleeping'}
+
+    def test_send_signal(self, tmpdir):
+        """
+        Sending a ``SIGINT`` signal to ``task.send_signal`` kills the child
+        process.
+        """
+        task = Task(
+            image_url=ROOTFS_URI,
+            args=['sleep', '5'],
             download_path=pathlib.Path(tmpdir.strpath),
         )
-        assert isinstance(task.process.pid, int)
+        task.send_signal(signal.SIGINT)
+        assert task.get_health() == {'exists': False, 'status': None}
+
+    def test_existing_task(self, tmpdir):
+        """
+        It is possible to get an existing task by its id.
+        """
+        task = Task(
+            image_url=ROOTFS_URI,
+            args=['sleep', '5'],
+            download_path=pathlib.Path(tmpdir.strpath),
+        )
+
+        other_task = Task(existing_task=task.id)
+        # Interrupting one task interrupts the other, so they are the same task
+        task.send_signal(signal.SIGINT)
+        assert other_task.get_health() == {'exists': False, 'status': None}

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest flake8 coveralls pytest-cov
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
certifi==2025.1.31
charset-normalizer==3.4.1
-e git+https://github.com/adamtheturtle/chroot_tasker.git@a11bb67cbc42ec7a8e05c1e501386e223d1d1e85#egg=Chroot_Tasker
click==6.6
coverage==7.8.0
coveralls==4.0.1
docopt==0.6.2
exceptiongroup==1.2.2
flake8==7.2.0
idna==3.10
iniconfig==2.1.0
mccabe==0.7.0
packaging==24.2
pluggy==1.5.0
psutil==4.1.0
pycodestyle==2.13.0
pyflakes==3.3.2
pytest==8.3.5
pytest-cov==6.0.0
requests==2.32.3
tomli==2.2.1
urllib3==2.3.0

name: chroot_tasker
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:
      - certifi==2025.1.31
      - charset-normalizer==3.4.1
      - click==6.6
      - coverage==7.8.0
      - coveralls==4.0.1
      - docopt==0.6.2
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - idna==3.10
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - packaging==24.2
      - pluggy==1.5.0
      - psutil==4.1.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.2
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - requests==2.32.3
      - tomli==2.2.1
      - urllib3==2.3.0
prefix: /opt/conda/envs/chroot_tasker

cli/tests/test_cli.py::TestCreate::test_create_task cli/tests/test_cli.py::TestCreate::test_send_signal_healthcheck tasker/tests/test_tasker.py::TestTask::test_create tasker/tests/test_tasker.py::TestTask::test_send_signal tasker/tests/test_tasker.py::TestTask::test_existing_task
tasker/tests/test_tasker.py::TestCreateFilestystemDir::test_filesystem_dir_created tasker/tests/test_tasker.py::TestCreateFilestystemDir::test_multiple_filesystems tasker/tests/test_tasker.py::TestCreateFilestystemDir::test_image_removed tasker/tests/test_tasker.py::TestRunChrootProcess::test_run_chroot_process tasker/tests/test_tasker.py::TestRunChrootProcess::test_process_returned tasker/tests/test_tasker.py::TestRunChrootProcess::test_default_io
README.rst cli/cli.py tasker/tasker.py
README.rst cli/cli.py tasker/tasker.py