diff --git a/yotta/lib/pack.py b/yotta/lib/pack.py
index ade5aae..f7ef9d0 100644
--- a/yotta/lib/pack.py
+++ b/yotta/lib/pack.py
@@ -257,6 +257,12 @@ class Pack(object):
         else:
             return None
 
+    def getKeywords(self):
+        if self.description:
+            return self.description.get('keywords', [])
+        else:
+            return []
+
     def _parseIgnoreFile(self, f):
         r = []
         for l in f:
diff --git a/yotta/options/__init__.py b/yotta/options/__init__.py
index 1482b43..aa66e80 100644
--- a/yotta/options/__init__.py
+++ b/yotta/options/__init__.py
@@ -11,6 +11,7 @@ from . import noninteractive
 from . import registry
 from . import target
 from . import config
+from . import force
 
 # this modifies argparse when it's imported:
 from . import parser
diff --git a/yotta/options/force.py b/yotta/options/force.py
new file mode 100644
index 0000000..33c8e6e
--- /dev/null
+++ b/yotta/options/force.py
@@ -0,0 +1,12 @@
+# Copyright 2014-2015 ARM Limited
+#
+# Licensed under the Apache License, Version 2.0
+# See LICENSE file for details.
+
+def addTo(parser):
+    parser.add_argument('-f', '--force', action='store_true', dest="force",
+        help='Force the operation to (try to) continue even in situations which '+
+             'would be an error.'
+    )
+
+
diff --git a/yotta/publish.py b/yotta/publish.py
index 28d999b..721d932 100644
--- a/yotta/publish.py
+++ b/yotta/publish.py
@@ -8,11 +8,50 @@ import logging
 
 # validate, , validate things, internal
 from .lib import validate
+# options, , shared options, internal
+import yotta.options as options
 
 def addOptions(parser):
-    # no options
+    options.force.addTo(parser)
+
+# python 2 + 3 compatibility
+try:
+    global input
+    input = raw_input
+except NameError:
     pass
 
+def prePublishCheck(p, force=False, interactive=True):
+    need_ok = False
+    if p.description.get('bin', None) is not None:
+        logging.warning(
+            'This is an executable application, not a re-usable library module. Other modules will not be able to depend on it!'
+        )
+        need_ok = True
+
+    official_keywords = [x for x in p.getKeywords() if x.endswith('-official')]
+    if len(official_keywords):
+        need_ok = True
+        for k in official_keywords:
+            prefix = k[:-len('-official')]
+            logging.warning(
+                ('You\'re publishing with the %s tag. Is this really an '+
+                'officially supported %s module? If not, please remove the %s '+
+                'tag from your %s file. If you are unsure, please ask on the '+
+                'issue tracker.') % (
+                    k, prefix, k, p.description_filename
+                )
+            )
+
+    if need_ok and not interactive:
+        logging.error('--noninteractive prevents user confirmation. Please re-run with --force')
+        return 1
+
+    if need_ok and not force:
+        input("If you still want to publish, press [enter] to continue.")
+
+    return 0
+
 def execCommand(args, following_args):
     p = validate.currentDirectoryModuleOrTarget()
     if not p:
@@ -22,17 +61,9 @@ def execCommand(args, following_args):
         logging.error('The working directory is not clean. Commit before publishing!')
         return 1
 
-    if p.description.get('bin', None) is not None:
-        logging.warning(
-            'This is an executable application, not a re-usable library module. Other modules will not be able to depend on it!'
-        )
-        # python 2 + 3 compatibility
-        try:
-            global input
-            input = raw_input
-        except NameError:
-            pass
-        raw_input("If you still want to publish it, press [enter] to continue.")
+    errcode = prePublishCheck(p, args.force, args.interactive)
+    if errcode and not args.force:
+        return errcode
 
     error = p.publish(args.registry)
     if error:

Discourage accidental use of mbed-official keyword
There have been several cases where target descriptions have been published with descriptions and keywords indicating that they are officially supported, when in fact they aren't.

yotta should warn when these keywords are used, to make sure that their use is intentional.
diff --git a/yotta/test/cli/test_publish.py b/yotta/test/cli/test_publish.py
index de93d77..11b713e 100644
--- a/yotta/test/cli/test_publish.py
+++ b/yotta/test/cli/test_publish.py
@@ -13,7 +13,7 @@ import tempfile
 # internal modules:
 from yotta.lib.fsutils import rmRf
 from . import cli
-
+from . import util
 
 Test_Target = "x86-osx-native,*"
 
@@ -54,6 +54,26 @@ Public_Module_JSON = '''{
 }'''
 
 
+Test_Publish = {
+'module.json':'''{
+  "name": "test-publish",
+  "version": "0.0.0",
+  "description": "Test yotta publish",
+  "author": "James Crosby <james.crosby@arm.com>",
+  "license": "Apache-2.0",
+  "keywords": ["mbed-official"],
+  "dependencies":{
+  }
+}''',
+'readme.md':'''##This is a test module used in yotta's test suite.''',
+'source/foo.c':'''#include "stdio.h"
+int foo(){
+    printf("foo!\\n");
+    return 7;
+}'''
+}
+
+
 class TestCLIPublish(unittest.TestCase):
     @classmethod
     def setUpClass(cls):
@@ -89,6 +109,15 @@ class TestCLIPublish(unittest.TestCase):
             else:
                 del os.environ['YOTTA_USER_SETTINGS_DIR']
 
+    def test_warnOfficialKeywords(self):
+        path = util.writeTestFiles(Test_Publish, True)
+
+        stdout, stderr, statuscode = cli.run(['-t', 'x86-linux-native', '--noninteractive', 'publish'], cwd=path)
+        self.assertNotEqual(statuscode, 0)
+        self.assertIn('Is this really an officially supported mbed module', stdout + stderr)
+
+        util.rmRf(path)
+
 if __name__ == '__main__':
     unittest.main()
 

pip install -e .[dev]
parse_log_pytest
pytest
pytest
apt-get update apt-get install -y gcc cmake ninja-build
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
argcomplete==1.0.0
certifi==2025.1.31
cffi==1.17.1
charset-normalizer==3.4.1
colorama==0.3.9
cryptography==44.0.2
Deprecated==1.2.18
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
future==1.0.0
hgapi==1.7.4
idna==3.10
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
intelhex==2.3.0
intervaltree==3.1.0
Jinja2==2.11.3
jsonpointer==2.0
jsonschema==2.6.0
MarkupSafe==3.0.2
mbed_test_wrapper==0.0.3
packaging @ file:///croot/packaging_1734472117206/work
pathlib==1.0.1
pluggy @ file:///croot/pluggy_1733169602837/work
project-generator-definitions==0.2.46
project_generator==0.8.17
pycparser==2.22
pyelftools==0.23
PyGithub==1.54.1
PyJWT==1.7.1
pyocd==0.15.0
pytest @ file:///croot/pytest_1738938843180/work
pyusb==1.3.1
PyYAML==3.13
requests==2.32.3
semantic-version==2.10.0
six==1.17.0
sortedcontainers==2.4.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
urllib3==2.3.0
valinor==0.0.15
websocket-client==1.8.0
wrapt==1.17.2
xmltodict==0.14.2
-e git+https://github.com/ARMmbed/yotta.git@16cc2baeba653dc77e3ce32c20018b32ab108bf4#egg=yotta

name: yotta
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_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
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=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
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argcomplete==1.0.0
      - argparse==1.4.0
      - certifi==2025.1.31
      - cffi==1.17.1
      - charset-normalizer==3.4.1
      - colorama==0.3.9
      - cryptography==44.0.2
      - deprecated==1.2.18
      - future==1.0.0
      - hgapi==1.7.4
      - idna==3.10
      - intelhex==2.3.0
      - intervaltree==3.1.0
      - jinja2==2.11.3
      - jsonpointer==2.0
      - jsonschema==2.6.0
      - markupsafe==3.0.2
      - mbed-test-wrapper==0.0.3
      - pathlib==1.0.1
      - project-generator==0.8.17
      - project-generator-definitions==0.2.46
      - pycparser==2.22
      - pyelftools==0.23
      - pygithub==1.54.1
      - pyjwt==1.7.1
      - pyocd==0.15.0
      - pyusb==1.3.1
      - pyyaml==3.13
      - requests==2.32.3
      - semantic-version==2.10.0
      - six==1.17.0
      - sortedcontainers==2.4.0
      - urllib3==2.3.0
      - valinor==0.0.15
      - websocket-client==1.8.0
      - wrapt==1.17.2
      - xmltodict==0.14.2
prefix: /opt/conda/envs/yotta

yotta/test/cli/test_publish.py::TestCLIPublish::test_warnOfficialKeywords
yotta/test/cli/test_publish.py::TestCLIPublish::test_publishNotAuthed
yotta/test/cli/test_publish.py::TestCLIPublish::test_publishPrivate
yotta/options/force.py yotta/publish.py yotta/options/__init__.py yotta/lib/pack.py
yotta/options/force.py yotta/publish.py yotta/options/__init__.py yotta/lib/pack.py