diff --git a/inlineplz/interfaces/github.py b/inlineplz/interfaces/github.py
index 005b8ff..4dd4397 100644
--- a/inlineplz/interfaces/github.py
+++ b/inlineplz/interfaces/github.py
@@ -26,14 +26,14 @@ class GitHubInterface(InterfaceBase):
     def post_messages(self, messages):
         messages_to_post = 0
         for msg in messages:
-            if not msg.content:
+            if not msg.comments:
                 continue
             msg_position = self.position(msg)
             if msg_position:
                 messages_to_post += 1
                 if not self.is_duplicate(msg, msg_position):
                     self.pull_request.create_review_comment(
-                        msg.content,
+                        self.format_message(msg),
                         self.last_sha,
                         msg.path,
                         msg_position
@@ -44,10 +44,22 @@ class GitHubInterface(InterfaceBase):
         for comment in self.pull_request.review_comments():
             if (comment.position == position and
                     comment.path == message.path and
-                    comment.body.strip() == message.content.strip()):
+                    comment.body.strip() == self.format_message(message).strip()):
                 return True
         return False
 
+    @staticmethod
+    def format_message(message):
+        if not message.comments:
+            return ''
+        if len(message.comments) > 1:
+            return (
+                '```\n' +
+                '\n'.join(sorted(list(message.comments))) +
+                '\n```'
+            )
+        return '`{0}`'.format(list(message.comments)[0].strip())
+
     def position(self, message):
         """Calculate position within the PR, which is not the line number"""
         patch = unidiff.PatchSet(self.diff.split('\n'))
diff --git a/inlineplz/linters/__init__.py b/inlineplz/linters/__init__.py
index 420508e..458fd7c 100644
--- a/inlineplz/linters/__init__.py
+++ b/inlineplz/linters/__init__.py
@@ -9,6 +9,7 @@ import subprocess
 import traceback
 
 from inlineplz import parsers
+from inlineplz import message
 
 LINTERS = {
     'prospector': {
@@ -44,7 +45,7 @@ LINTERS = {
 
 
 def lint(install=False):
-    messages = []
+    messages = message.Messages()
     for config in LINTERS.values():
         if any(dotfile in os.listdir(os.getcwd())
                for dotfile in config.get('dotfiles')):
@@ -52,7 +53,7 @@ def lint(install=False):
                 if install and config.get('install'):
                     subprocess.check_call(config.get('install'))
                 output = subprocess.check_output(config.get('run')).decode('utf-8')
-                messages.extend(config.get('parser')().parse(output))
+                messages.add_messages(config.get('parser')().parse(output))
             except subprocess.CalledProcessError:
                 traceback.print_exc()
-    return messages
+    return messages.get_messages()
diff --git a/inlineplz/message.py b/inlineplz/message.py
index 61011a4..da2c722 100644
--- a/inlineplz/message.py
+++ b/inlineplz/message.py
@@ -1,12 +1,32 @@
 # -*- coding: utf-8 -*-
 
+import os
+
+
+class Messages(object):
+
+    def __init__(self):
+        self.messages = {}
+
+    def add_message(self, path, line, message):
+        if (path, line) not in self.messages:
+            self.messages[(path, line)] = Message(path, line)
+        self.messages[(path, line)].append(message)
+
+    def add_messages(self, messages):
+        for message in messages:
+            self.add_message(*message)
+
+    def get_messages(self):
+        return self.messages.values()
+
 
 class Message(object):
 
     def __init__(self, path, line_number):
-        self.path = path.replace('\\', '/')
+        self.path = os.path.relpath(path).replace('\\', '/')
         self.line_number = line_number
-        self.comments = []
+        self.comments = set()
 
     def __str__(self):
         return """
@@ -16,10 +36,5 @@ Message:
     Content: {2}
         """.format(self.path, self.line_number, self.content).strip()
 
-    @property
-    def content(self):
-        if not self.comments:
-            return ''
-        if len(self.comments) > 1:
-            return '```\n' + '\n'.join(self.comments) + '\n```'
-        return '`{0}`'.format(self.comments[0].strip())
+    def append(self, message):
+        self.comments.add(message)
diff --git a/inlineplz/parsers/eslint.py b/inlineplz/parsers/eslint.py
index 6a0c211..f30d7b2 100644
--- a/inlineplz/parsers/eslint.py
+++ b/inlineplz/parsers/eslint.py
@@ -5,24 +5,21 @@ from collections import OrderedDict
 import json
 
 from inlineplz.parsers.base import ParserBase
-from inlineplz.message import Message
 
 
 class ESLintParser(ParserBase):
     """Parse json eslint output."""
 
     def parse(self, lint_data):
-        messages = []
+        messages = set()
         for filedata in json.loads(
             lint_data,
             object_pairs_hook=OrderedDict
         ):
             if filedata.get('messages'):
                 for msgdata in filedata['messages']:
-                    msg = Message(
-                        filedata.get('filePath'),
-                        msgdata.get('line')
-                    )
-                    msg.comments.append(msgdata.get('message'))
-                    messages.append(msg)
+                    path = filedata['filePath']
+                    line = msgdata['line']
+                    msgbody = msgdata['message']
+                    messages.add((path, line, msgbody))
         return messages
diff --git a/inlineplz/parsers/jscs.py b/inlineplz/parsers/jscs.py
index e3fd72c..b80d8db 100644
--- a/inlineplz/parsers/jscs.py
+++ b/inlineplz/parsers/jscs.py
@@ -5,24 +5,21 @@ from collections import OrderedDict
 import json
 
 from inlineplz.parsers.base import ParserBase
-from inlineplz.message import Message
 
 
 class JSCSParser(ParserBase):
     """Parse json jscs output."""
 
     def parse(self, lint_data):
-        messages = []
+        messages = set()
         for filename, msgs in json.loads(
             lint_data,
             object_pairs_hook=OrderedDict
         ).items():
             if msgs:
                 for msgdata in msgs:
-                    msg = Message(
-                        filename,
-                        msgdata.get('line')
-                    )
-                    msg.comments.append(msgdata.get('message'))
-                    messages.append(msg)
+                    path = filename
+                    line = msgdata['line']
+                    msgbody = msgdata['message']
+                    messages.add((path, line, msgbody))
         return messages
diff --git a/inlineplz/parsers/jshint.py b/inlineplz/parsers/jshint.py
index 9c8beee..7e8863f 100644
--- a/inlineplz/parsers/jshint.py
+++ b/inlineplz/parsers/jshint.py
@@ -4,24 +4,21 @@ from __future__ import absolute_import
 import xmltodict
 
 from inlineplz.parsers.base import ParserBase
-from inlineplz.message import Message
 
 
 class JSHintParser(ParserBase):
     """Parse json jshint output."""
 
     def parse(self, lint_data):
-        messages = []
+        messages = set()
         obj = xmltodict.parse(lint_data)
         for filedata in obj['checkstyle']['file']:
             for errordata in filedata['error']:
                 try:
-                    msg = Message(
-                        filedata.get('@name'),
-                        int(errordata.get('@line'))
-                    )
-                    msg.comments.append(errordata.get('@message'))
-                    messages.append(msg)
-                except AttributeError:
+                    path = filedata['@name']
+                    line = int(errordata['@line'])
+                    msgbody = errordata['@message']
+                    messages.add((path, line, msgbody))
+                except (AttributeError, TypeError):
                     pass
         return messages
diff --git a/inlineplz/parsers/prospector.py b/inlineplz/parsers/prospector.py
index 8146c2a..43b8eb6 100644
--- a/inlineplz/parsers/prospector.py
+++ b/inlineplz/parsers/prospector.py
@@ -5,27 +5,23 @@ from collections import OrderedDict
 import json
 
 from inlineplz.parsers.base import ParserBase
-from inlineplz.message import Message
 
 
 class ProspectorParser(ParserBase):
     """Parse json prospector output."""
 
     def parse(self, lint_data):
-        messages = []
+        messages = set()
         for msgdata in json.loads(
             lint_data,
             object_pairs_hook=OrderedDict
         ).get('messages'):
-            msg = Message(
-                msgdata['location']['path'],
-                msgdata['location']['line']
-            )
+            path = msgdata['location']['path']
+            line = msgdata['location']['line']
             msgbody = '{0}: {1} ({2})'.format(
                 msgdata['source'],
                 msgdata['message'],
                 msgdata['code']
             )
-            msg.comments.append(msgbody)
-            messages.append(msg)
+            messages.add((path, line, msgbody))
         return messages

move message formatting logic from message module into interface modules
diff --git a/tests/parsers/test_eslint.py b/tests/parsers/test_eslint.py
index d8e765b..289099d 100644
--- a/tests/parsers/test_eslint.py
+++ b/tests/parsers/test_eslint.py
@@ -15,7 +15,7 @@ eslint_path = os.path.join(
 
 def test_eslint():
     with open(eslint_path) as inputfile:
-        messages = eslint.ESLintParser().parse(inputfile.read())
-        assert messages[0].content == '`Parsing error: Illegal return statement`'
-        assert messages[0].line_number == 17
-        assert messages[0].path == 'C:/Users/Guy/Documents/jshint/tests/unit/fixtures/asi.js'
+        messages = sorted(list(eslint.ESLintParser().parse(inputfile.read())))
+        assert messages[0][2] == 'Parsing error: Illegal return statement'
+        assert messages[0][1] == 17
+        assert messages[0][0] == 'C:\\Users\\Guy\\Documents\\jshint\\tests\\unit\\fixtures\\asi.js'
diff --git a/tests/parsers/test_jscs.py b/tests/parsers/test_jscs.py
index e0577f6..d71c08f 100644
--- a/tests/parsers/test_jscs.py
+++ b/tests/parsers/test_jscs.py
@@ -15,7 +15,7 @@ jscs_path = os.path.join(
 
 def test_jscs():
     with open(jscs_path) as inputfile:
-        messages = jscs.JSCSParser().parse(inputfile.read())
-        assert messages[0].content == '`maximumLineLength: Line must be at most 100 characters`'
-        assert messages[0].line_number == 1
-        assert messages[0].path == './data/non-ascii-identifier-part-only.js'
+        messages = sorted(list(jscs.JSCSParser().parse(inputfile.read())))
+        assert messages[0][2] == 'maximumLineLength: Line must be at most 100 characters'
+        assert messages[0][1] == 1
+        assert messages[0][0] == './data/non-ascii-identifier-part-only.js'
diff --git a/tests/parsers/test_jshint.py b/tests/parsers/test_jshint.py
index fb4c4c6..f1d8c79 100644
--- a/tests/parsers/test_jshint.py
+++ b/tests/parsers/test_jshint.py
@@ -15,7 +15,7 @@ jshint_path = os.path.join(
 
 def test_jshint():
     with open(jshint_path) as inputfile:
-        messages = jshint.JSHintParser().parse(inputfile.read())
-        assert messages[0].content == '`Use the function form of "use strict". (W097)`'
-        assert messages[0].line_number == 8
-        assert messages[0].path == 'conf/cli-options.js'
+        messages = sorted(list(jshint.JSHintParser().parse(inputfile.read())))
+        assert messages[0][2] == 'Use the function form of "use strict". (W097)'
+        assert messages[0][1] == 7
+        assert messages[0][0] == 'Makefile.js'
diff --git a/tests/parsers/test_prospector.py b/tests/parsers/test_prospector.py
index fbba037..4b3e569 100644
--- a/tests/parsers/test_prospector.py
+++ b/tests/parsers/test_prospector.py
@@ -16,13 +16,15 @@ prospector_path = os.path.join(
 
 def test_prospector():
     with open(prospector_path) as inputfile:
-        messages = prospector.ProspectorParser().parse(inputfile.read())
-        assert messages[0].content == '`pep257: Missing docstring in public package (D104)`'
-        assert messages[0].line_number == 1
-        assert messages[0].path == 'inlineplz/util/__init__.py'
-        assert messages[1].content == '`pep257: Missing docstring in public package (D104)`'
-        assert messages[1].line_number == 1
-        assert messages[1].path == 'inlineplz/parsers/__init__.py'
-        assert messages[9].content == ('`pep257: One-line docstring should fit on one line with quotes (found 2) (D200)`')
-        assert messages[9].line_number == 1
+        messages = sorted(list(prospector.ProspectorParser().parse(inputfile.read())))
+        assert messages[0][2] == 'pep257: Missing docstring in public package (D104)'
+        assert messages[0][1] == 1
+        assert messages[0][0] == 'inlineplz\__init__.py'
+
+        assert messages[1][2] == 'pep257: Missing docstring in public package (D104)'
+        assert messages[1][1] == 1
+        assert messages[1][0] == 'inlineplz\interfaces\__init__.py'
+
+        assert messages[9][2] == 'pep257: Missing docstring in public package (D104)'
+        assert messages[9][1] == 1
         assert len(messages) == 32

pip install -e .[dev]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc
3.5
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
cryptography==40.0.2
github3.py==3.2.0
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
-e git+https://github.com/guykisel/inline-plz.git@3cfa8f2c3f0fd814c105ca4d51f0727659a45fa8#egg=inlineplz
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycparser==2.21
PyJWT==2.4.0
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
python-dateutil==2.9.0.post0
requests==2.27.1
six==1.17.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
unidiff==0.7.5
uritemplate==4.1.1
urllib3==1.26.20
xmltodict==0.14.2
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: inline-plz
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
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - cryptography==40.0.2
      - github3-py==3.2.0
      - idna==3.10
      - pycparser==2.21
      - pyjwt==2.4.0
      - python-dateutil==2.9.0.post0
      - requests==2.27.1
      - six==1.17.0
      - unidiff==0.7.5
      - uritemplate==4.1.1
      - urllib3==1.26.20
      - xmltodict==0.14.2
prefix: /opt/conda/envs/inline-plz

tests/parsers/test_eslint.py::test_eslint tests/parsers/test_jscs.py::test_jscs tests/parsers/test_jshint.py::test_jshint tests/parsers/test_prospector.py::test_prospector
inlineplz/parsers/eslint.py inlineplz/parsers/jscs.py inlineplz/parsers/prospector.py inlineplz/linters/__init__.py inlineplz/interfaces/github.py inlineplz/message.py inlineplz/parsers/jshint.py
inlineplz/parsers/eslint.py inlineplz/parsers/jscs.py inlineplz/parsers/prospector.py inlineplz/linters/__init__.py inlineplz/interfaces/github.py inlineplz/message.py inlineplz/parsers/jshint.py